'use strict'; const path = require('node:path'); const { app, BrowserWindow, shell, session, safeStorage } = require('electron'); const { ConfigStore } = require('./src/main/config-store.cjs'); const { GitService } = require('./src/main/git-service.cjs'); const { GiteaService } = require('./src/main/gitea-service.cjs'); const { RepositoryService } = require('./src/main/repository-service.cjs'); const { DeploymentService } = require('./src/main/deployment-service.cjs'); const { RepositoryMonitor } = require('./src/main/repository-monitor.cjs'); const { DiagnosticsService } = require('./src/main/diagnostics-service.cjs'); const { PreflightService } = require('./src/main/preflight-service.cjs'); const { UpdateService } = require('./src/main/update-service.cjs'); const { SshService } = require('./src/main/ssh-service.cjs'); const { UnraidDeploymentService } = require('./src/main/unraid-deployment-service.cjs'); const { registerIpc } = require('./src/main/ipc.cjs'); let mainWindow; let repositoryMonitor; let operationTimer; let diagnostics; let quitCleanupStarted = false; function broadcast(channel, payload) { for (const window of BrowserWindow.getAllWindows()) { if (!window.isDestroyed()) window.webContents.send(channel, payload); } } function createWindow() { mainWindow = new BrowserWindow({ width: 1480, height: 940, minWidth: 1120, minHeight: 720, show: false, backgroundColor: '#0b0e14', title: 'ForgeFlow', icon: path.join(__dirname, 'build', 'icon.png'), autoHideMenuBar: true, titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true, webSecurity: true, spellcheck: false } }); mainWindow.loadFile(path.join(__dirname, 'src', 'renderer', 'index.html')); mainWindow.once('ready-to-show', () => { mainWindow.show(); diagnostics?.info('window.ready', { size: mainWindow.getSize() }); }); mainWindow.on('unresponsive', () => diagnostics?.warning('window.unresponsive', {})); mainWindow.webContents.on('render-process-gone', (_event, details) => diagnostics?.error('renderer.process.gone', details)); mainWindow.webContents.on('did-fail-load', (_event, code, description, validatedUrl) => diagnostics?.error('renderer.load.failed', { code, description, validatedUrl })); mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (/^https?:\/\//i.test(url)) shell.openExternal(url); return { action: 'deny' }; }); mainWindow.webContents.on('will-navigate', (event, url) => { if (url !== mainWindow.webContents.getURL()) event.preventDefault(); }); } app.whenReady().then(async () => { session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': [ "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'" ] } }); }); const userDataPath = app.getPath('userData'); const store = new ConfigStore(userDataPath); await store.load(); diagnostics = new DiagnosticsService({ userDataPath, appInfo: { name: app.getName(), version: app.getVersion(), packaged: app.isPackaged }, secretProvider: () => [ store.getToken(), ...(store.data.servers || []).flatMap((server) => { try { const credentials = store.getServerCredentials(server.id); return [credentials.password, credentials.passphrase]; } catch { return []; } }) ], preferencesProvider: () => store.data.preferences }); await diagnostics.initialize(); process.on('uncaughtException', (error) => { diagnostics?.error('process.uncaught-exception', error).finally(() => app.exit(1)); }); process.on('unhandledRejection', (reason) => diagnostics?.error('process.unhandled-rejection', reason instanceof Error ? reason : { reason })); const git = new GitService(); const gitea = new GiteaService(store, diagnostics); const repositories = new RepositoryService(store, git, gitea, diagnostics); const deployments = new DeploymentService(store, gitea, git, diagnostics); const ssh = new SshService({ store, diagnostics }); const unraid = new UnraidDeploymentService({ store, ssh, git, diagnostics, sourcePath: app.getAppPath(), onOperationChange: (payload) => broadcast('operations:changed', payload) }); const updates = new UpdateService({ store, gitea, diagnostics, appInfo: { version: app.getVersion(), packaged: app.isPackaged }, sourcePath: app.getAppPath(), userDataPath }); const preflight = new PreflightService({ store, git, gitea, deployments, diagnostics, userDataPath, secureStorageAvailable: () => safeStorage.isEncryptionAvailable() }); repositoryMonitor = new RepositoryMonitor({ store, git, diagnostics, onChange: (payload) => broadcast('repositories:changed', payload) }); repositoryMonitor.restart(); registerIpc({ store, git, gitea, repositories, deployments, unraid, ssh, updates, preflight, diagnostics, monitor: repositoryMonitor }); createWindow(); if (store.data.setupComplete && store.data.updates?.autoCheck && store.getToken()) { setTimeout(async () => { try { const status = await updates.check(); broadcast('updates:changed', status); } catch (error) { await diagnostics.warning('updates.startup-check.failed', { message: error.message, code: error.code }); } }, 2500).unref?.(); } const gitAvailability = await git.isAvailable(); await diagnostics.info('app.ready', { platform: process.platform, arch: process.arch, setupComplete: store.data.setupComplete, git: gitAvailability, secureStorageAvailable: safeStorage.isEncryptionAvailable() }); const scheduleOperationPoll = () => { if (operationTimer) clearTimeout(operationTimer); const intervalMs = Math.max(3, Number(store.data.preferences.operationPollSeconds) || 5) * 1000; operationTimer = setTimeout(async () => { if (store.data.setupComplete) { const active = store.data.operations.some((item) => item.type === 'deployment' && !['success', 'failed', 'cancelled', 'rolled-back'].includes(item.status)); if (active) { const [actions, sshOperations] = await Promise.all([ store.getToken() ? deployments.refreshActiveOperations().catch(async (error) => { await diagnostics.error('operation-monitor.actions.failed', error); return []; }) : [], unraid.refreshActiveOperations().catch(async (error) => { await diagnostics.error('operation-monitor.unraid.failed', error); return []; }) ]); const updated = [...actions, ...sshOperations]; if (updated.length) broadcast('operations:changed', { operations: updated }); } } scheduleOperationPoll(); }, intervalMs); operationTimer.unref?.(); }; scheduleOperationPoll(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }).catch(async (error) => { console.error('[startup]', error); await diagnostics?.error('app.startup.failed', error); await diagnostics?.flush(); app.exit(1); }); app.on('before-quit', (event) => { if (quitCleanupStarted) return; event.preventDefault(); quitCleanupStarted = true; repositoryMonitor?.stop(); if (operationTimer) clearTimeout(operationTimer); Promise.resolve() .then(() => diagnostics?.info('app.quitting', {})) .then(() => diagnostics?.flush()) .finally(() => app.quit()); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });