Nouveau paquet packages/desktop (Electron, autonome, HORS des workspaces racine pour ne pas alourdir la CI du daemon). Process principal : lance le daemon en enfant (Node), lit le handshake token sur le fd 3, pose le cookie de session server-to-server (session.fetch) puis charge la SPA sur 127.0.0.1 (aucun écran de login). Fenêtre sûre (contextIsolation, sandbox, nodeIntegration off, preload minimal), instance unique, liens externes vers le navigateur, permissions limitées aux notifications, état de fenêtre persistant, arrêt propre du daemon (SIGTERM puis SIGKILL). Vérifié ici : tsc contre l'API Electron réelle + bundle esbuild (main/preload). Le run GUI et le packaging se font sur une machine avec affichage (C2+). Racine workspaces passée en liste explicite (5 paquets) pour exclure desktop du npm ci racine.
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
import { app, BrowserWindow, session, shell, type BrowserWindowConstructorOptions } from 'electron';
|
|
import { join } from 'node:path';
|
|
import { startDaemon, type DaemonHandle } from './daemon';
|
|
import { seedSessionCookie } from './auth';
|
|
import { loadWindowState, saveWindowState } from './window-state';
|
|
|
|
const PARTITION = 'persist:arboretum';
|
|
const PORT = 7317;
|
|
|
|
let daemon: DaemonHandle | null = null;
|
|
let win: BrowserWindow | null = null;
|
|
let shuttingDown = false;
|
|
|
|
// Instance unique : deux instances = deux daemons/ports en conflit.
|
|
if (!app.requestSingleInstanceLock()) {
|
|
app.quit();
|
|
} else {
|
|
app.on('second-instance', () => {
|
|
if (!win) return;
|
|
if (win.isMinimized()) win.restore();
|
|
win.focus();
|
|
});
|
|
app.whenReady().then(bootstrap).catch((err: unknown) => {
|
|
console.error('[arboretum-desktop] bootstrap failed:', err);
|
|
app.quit();
|
|
});
|
|
}
|
|
|
|
async function bootstrap(): Promise<void> {
|
|
const dataDir = join(app.getPath('userData'), 'daemon');
|
|
daemon = await startDaemon({ dataDir, port: PORT, onLog: (l) => process.stdout.write(l) });
|
|
await seedSessionCookie(PARTITION, daemon.url, daemon.token);
|
|
createWindow(daemon.url);
|
|
}
|
|
|
|
function createWindow(url: string): void {
|
|
const state = loadWindowState();
|
|
const opts: BrowserWindowConstructorOptions = {
|
|
width: state.width,
|
|
height: state.height,
|
|
backgroundColor: '#09090b',
|
|
webPreferences: {
|
|
partition: PARTITION,
|
|
contextIsolation: true,
|
|
sandbox: true,
|
|
nodeIntegration: false,
|
|
preload: join(__dirname, 'preload.js'),
|
|
},
|
|
};
|
|
if (state.x !== undefined) opts.x = state.x;
|
|
if (state.y !== undefined) opts.y = state.y;
|
|
|
|
win = new BrowserWindow(opts);
|
|
if (state.maximized) win.maximize();
|
|
|
|
// Liens externes -> navigateur système ; toute navigation hors origine locale est déviée.
|
|
win.webContents.setWindowOpenHandler(({ url: u }) => {
|
|
void shell.openExternal(u);
|
|
return { action: 'deny' };
|
|
});
|
|
win.webContents.on('will-navigate', (e, u) => {
|
|
if (!u.startsWith(url)) {
|
|
e.preventDefault();
|
|
void shell.openExternal(u);
|
|
}
|
|
});
|
|
|
|
// Permissions : n'autoriser que les notifications (cohérent avec la Permissions-Policy serveur).
|
|
session.fromPartition(PARTITION).setPermissionRequestHandler((_wc, permission, cb) => cb(permission === 'notifications'));
|
|
|
|
const persist = (): void => {
|
|
if (!win) return;
|
|
const b = win.getBounds();
|
|
saveWindowState({ width: b.width, height: b.height, x: b.x, y: b.y, maximized: win.isMaximized() });
|
|
};
|
|
win.on('close', persist);
|
|
win.on('closed', () => {
|
|
win = null;
|
|
});
|
|
|
|
void win.loadURL(`${url}/`);
|
|
}
|
|
|
|
app.on('window-all-closed', () => void shutdown());
|
|
app.on('before-quit', (e) => {
|
|
// Ne pas quitter avant l'arrêt propre du daemon enfant.
|
|
if (daemon && !shuttingDown) {
|
|
e.preventDefault();
|
|
void shutdown();
|
|
}
|
|
});
|
|
|
|
async function shutdown(): Promise<void> {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
try {
|
|
await daemon?.stop();
|
|
} finally {
|
|
daemon = null;
|
|
app.quit();
|
|
}
|
|
}
|