203 lines
7.4 KiB
JavaScript
203 lines
7.4 KiB
JavaScript
'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 });
|
|
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 && store.getToken()) {
|
|
const active = store.data.operations.some((item) => item.type === 'deployment' && !['success', 'failed', 'cancelled', 'rolled-back'].includes(item.status));
|
|
if (active) {
|
|
const updated = await deployments.refreshActiveOperations().catch(async (error) => {
|
|
await diagnostics.error('operation-monitor.failed', error);
|
|
return [];
|
|
});
|
|
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();
|
|
});
|