From ba0b99cd9c5518b847f5212cd406d99a2fb4ef2f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 16:00:15 +0100 Subject: [PATCH] fix: implement ASAR extraction workaround for Linux tray icon visibility and finalize main process hardening --- frontend/electron/main.ts | 243 +++++++++++++++++++++++--------------- 1 file changed, 150 insertions(+), 93 deletions(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index f41c8be..8e1cf0f 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -11,32 +11,58 @@ import { scrapeCooldown, SteamAuthError } from './services/scraper'; import { steamClient, LocalSteamAccount } from './services/steam-client'; import { BackendService } from './services/backend'; -// --- Reliable isDev check --- +// Reliable isDev check const isDev = !app.isPackaged; + app.name = "Ultimate Ban Tracker"; +// Force Wayland/Ozone support if on Linux +if (process.platform === 'linux') { + app.commandLine.appendSwitch('enable-features', 'UseOzonePlatform'); + app.commandLine.appendSwitch('ozone-platform', 'wayland'); +} + // Load environment variables dotenv.config({ path: path.join(app.getAppPath(), '..', '.env') }); // --- Types & Interfaces --- interface Account { - _id: string; steamId: string; personaName: string; loginName: string; - steamLoginSecure?: string; loginConfig?: any; sessionUpdatedAt?: string; - autoCheckCooldown: boolean; avatar: string; localAvatar?: string; - profileUrl: string; status: string; vacBanned: boolean; gameBans: number; - lastBanCheck: string; lastScrapeTime?: string; cooldownExpiresAt?: string; - authError?: boolean; notes?: string; sharedWith?: any[]; + _id: string; + steamId: string; + personaName: string; + loginName: string; + steamLoginSecure?: string; + loginConfig?: any; + sessionUpdatedAt?: string; + autoCheckCooldown: boolean; + avatar: string; + localAvatar?: string; + profileUrl: string; + status: string; + vacBanned: boolean; + gameBans: number; + lastBanCheck: string; + lastScrapeTime?: string; + cooldownExpiresAt?: string; + authError?: boolean; + notes?: string; + sharedWith?: any[]; } interface ServerConfig { - url: string; token?: string; serverSteamId?: string; enabled: boolean; theme?: string; isAdmin?: boolean; + url: string; + token?: string; + serverSteamId?: string; + enabled: boolean; + theme?: string; + isAdmin?: boolean; } -// --- 1. GLOBAL REFERENCES (Prevent Garbage Collection) --- +// --- App State --- let mainWindow: BrowserWindow | null = null; let tray: Tray | null = null; -let isQuitting = false; let backend: BackendService | null = null; +(app as any).isQuitting = false; const store = new Store<{ accounts: Account[], serverConfig: ServerConfig }>({ defaults: { @@ -45,7 +71,7 @@ const store = new Store<{ accounts: Account[], serverConfig: ServerConfig }>({ } }) as any; -// --- Avatar Cache --- +// --- Avatar Cache Logic --- const AVATAR_DIR = path.join(app.getPath('userData'), 'avatars'); if (!fs.existsSync(AVATAR_DIR)) fs.mkdirSync(AVATAR_DIR, { recursive: true }); @@ -67,19 +93,68 @@ const initBackend = () => { } else { backend = null; } }; -// --- 2. TRAY IMPLEMENTATION (Smart Arch) --- -const getIconPath = (themeName: string = 'steam') => { - if (process.platform === 'win32') { - // Windows prefers .ico, but we use high-res PNG as fallback if .ico isn't generated - return path.join(app.getAppPath(), 'assets-build', 'icon.png'); +// --- System Tray --- +const createTray = () => { + console.log('[Tray] Initializing...'); + + // 1. Determine source path (handling both Dev and Prod/ASAR) + let sourceIconPath = ''; + const possibleSourcePaths = [ + path.join(app.getAppPath(), 'assets-build', 'icon.png'), // Priority 1: ASAR/Internal + path.join(__dirname, '..', 'assets-build', 'icon.png'), // Priority 2: Dev + path.join(process.resourcesPath, 'assets-build', 'icon.png') // Priority 3: External resources + ]; + + for (const p of possibleSourcePaths) { + if (fs.existsSync(p)) { sourceIconPath = p; break; } + } + + if (!sourceIconPath) { + console.warn('[Tray] FAILED: No source icon found. Using empty fallback.'); + try { tray = new Tray(nativeImage.createEmpty()); } catch (e) {} + return; + } + + // 2. LINUX FIX: Extract icon from ASAR to real filesystem + let finalIconPath = sourceIconPath; + if (process.platform === 'linux') { + try { + const tempIconPath = path.join(app.getPath('temp'), 'ultimate-ban-tracker-tray.png'); + try { fs.unlinkSync(tempIconPath); } catch (e) {} + + console.log(`[Tray] Extracting icon to: ${tempIconPath}`); + fs.copyFileSync(sourceIconPath, tempIconPath); + finalIconPath = tempIconPath; + } catch (e: any) { + console.error(`[Tray] Failed to extract icon: ${e.message}`); + } } - // Linux: Try system path first (Most reliable for Wayland/StatusNotifier) - const systemPath = '/usr/share/pixmaps/ultimate-ban-tracker.png'; - if (fs.existsSync(systemPath)) return systemPath; + try { + console.log(`[Tray] Creating tray with icon: ${finalIconPath}`); + const icon = nativeImage.createFromPath(finalIconPath).resize({ width: 16, height: 16 }); + tray = new Tray(icon); + tray.setToolTip('Ultimate Ban Tracker'); + + if (process.platform === 'linux') { + tray.setIgnoreMouseEvents(false); + } - // Fallback to local assets - return path.join(app.getAppPath(), 'assets-build', 'icon.png'); + tray.on('click', () => { + if (mainWindow) { + mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); + } + }); + + updateTrayMenu(); + + const config = store.get('serverConfig'); + if (config?.theme) setAppIcon(config.theme); + + console.log(`[Tray] Successfully initialized`); + } catch (e: any) { + console.error(`[Tray] Error: ${e.message}`); + } }; const updateTrayMenu = () => { @@ -100,24 +175,25 @@ const updateTrayMenu = () => { }, { label: 'Sync Now', enabled: !!config?.enabled, click: () => syncAccounts(true) }, { type: 'separator' }, - { label: 'Show Dashboard', click: () => { if (mainWindow) mainWindow.show(); } }, - { label: 'Quit', click: () => { isQuitting = true; app.quit(); } } + { label: 'Show Dashboard', click: () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } } }, + { label: 'Quit', click: () => { + (app as any).isQuitting = true; + if (tray) tray.destroy(); + app.quit(); + } } ]); tray.setContextMenu(contextMenu); }; -const setupTray = () => { - const icon = nativeImage.createFromPath(getIconPath()).resize({ width: 16, height: 16 }); - tray = new Tray(icon); - tray.setToolTip('Ultimate Ban Tracker'); +const setAppIcon = (themeName: string = 'steam') => { + const assetsDir = path.join(app.getAppPath(), 'assets-build', 'icons'); + const iconPath = path.join(assetsDir, `${themeName}.svg`); - tray.on('click', () => { - if (!mainWindow) return; - mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); - }); - - updateTrayMenu(); + if (!fs.existsSync(iconPath)) return; + const icon = nativeImage.createFromPath(iconPath); + if (tray) tray.setImage(icon.resize({ width: 16, height: 16 })); + if (mainWindow) mainWindow.setIcon(icon); }; // --- Steam Logic --- @@ -194,6 +270,7 @@ const scrapeAccountData = async (account: Account) => { // --- Sync Worker --- const syncAccounts = async (isManual = false) => { + console.log(`[Sync] Starting ${isManual ? 'MANUAL' : 'BACKGROUND'} phase 1 (Server Pull)...`); initBackend(); let accounts = store.get('accounts') as Account[]; let hasChanges = false; @@ -245,11 +322,12 @@ const syncAccounts = async (isManual = false) => { if (JSON.stringify(exists.sharedWith) !== JSON.stringify(s.sharedWith)) { exists.sharedWith = s.sharedWith; hasChanges = true; } } } - } catch (e) { } + } catch (e) { console.error('[Sync] Pull failed:', e); } } if (hasChanges) { store.set('accounts', accounts); if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts); updateTrayMenu(); } const runScrapes = async () => { + console.log(`[Sync] Starting phase 2 (Scrapes) for ${accounts.length} accounts...`); const currentAccounts = [...store.get('accounts') as Account[]]; let scrapeChanges = false; for (const account of currentAccounts) { @@ -267,46 +345,13 @@ const syncAccounts = async (isManual = false) => { } catch (error) { } } if (scrapeChanges) { store.set('accounts', currentAccounts); if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts); updateTrayMenu(); } + console.log('[Sync] All phases complete.'); }; if (isManual) await runScrapes(); else runScrapes(); }; const scheduleNextSync = () => { setTimeout(async () => { await syncAccounts(false); scheduleNextSync(); }, isDev ? 300000 : 1800000); }; -// --- Discovery --- -const addingAccounts = new Set(); -const handleLocalAccountsFound = async (localAccounts: LocalSteamAccount[]) => { - const currentAccounts = store.get('accounts') as Account[]; - let hasChanges = false; - for (const local of localAccounts) { - if (addingAccounts.has(local.steamId)) continue; - const exists = currentAccounts.find(a => a.steamId === local.steamId); - if (exists) { if (!exists.loginName && local.accountName) { exists.loginName = local.accountName; hasChanges = true; } - } else { - addingAccounts.add(local.steamId); - try { - const profile = await fetchProfileData(local.steamId); - const bans = await scrapeBanStatus(profile.profileUrl); - const localPath = await downloadAvatar(profile.steamId, profile.avatar); - let loginConfig = undefined; - for (let i = 0; i < 3; i++) { await new Promise(r => setTimeout(r, 2000)); loginConfig = steamClient.extractAccountConfig(local.accountName); if (loginConfig) break; } - currentAccounts.push({ - _id: Date.now().toString() + Math.random().toString().slice(2, 5), - steamId: local.steamId, personaName: profile.personaName || local.accountName, - loginName: local.accountName, autoCheckCooldown: false, avatar: profile.avatar, - localAvatar: localPath, profileUrl: profile.profileUrl, - loginConfig, sessionUpdatedAt: loginConfig ? new Date().toISOString() : undefined, - status: (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none', - vacBanned: bans.vacBanned, gameBans: bans.gameBans, lastBanCheck: new Date().toISOString() - }); - hasChanges = true; - } catch (e) { } - addingAccounts.delete(local.steamId); - } - } - if (hasChanges) { store.set('accounts', currentAccounts); if (mainWindow) mainWindow.webContents.send('accounts-updated', currentAccounts); updateTrayMenu(); } -}; - // --- Single Instance & Window --- const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { @@ -329,25 +374,8 @@ if (!gotTheLock) { try { return net.fetch(pathToFileURL(absolutePath).toString()); } catch (e) { return new Response('Error', { status: 500 }); } }); - mainWindow = new BrowserWindow({ - width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, - webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true } - }); - - mainWindow.setMenu(null); - - mainWindow.on('close', (event) => { - if (!isQuitting) { - event.preventDefault(); - mainWindow?.hide(); - } - return false; - }); - - if (isDev) mainWindow.loadURL('http://localhost:5173'); - else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); - - setupTray(); + createWindow(); + createTray(); initBackend(); setTimeout(() => syncAccounts(false), 5000); scheduleNextSync(); @@ -355,14 +383,43 @@ if (!gotTheLock) { }); } -app.on('before-quit', () => { isQuitting = true; if (tray) tray.destroy(); }); -app.on('window-all-closed', () => { if (process.platform !== 'darwin' && isQuitting) app.quit(); }); -app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0 && gotTheLock) { /* Should be handled by ready */ } else mainWindow?.show(); }); +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1280, height: 800, title: "Ultimate Ban Tracker", backgroundColor: '#171a21', autoHideMenuBar: true, + webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true } + }); + + mainWindow.setMenu(null); + + mainWindow.on('close', (event) => { + if (!isQuitting) { + event.preventDefault(); + mainWindow?.hide(); + } + return false; + }); + + if (isDev) mainWindow.loadURL('http://localhost:5173'); + else mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); +} + +app.on('before-quit', () => { + console.log('[App] Preparing to quit...'); + (app as any).isQuitting = true; + if (tray) tray.destroy(); +}); + +app.on('window-all-closed', () => { if (process.platform !== 'darwin' && (app as any).isQuitting) app.quit(); }); +app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0 && gotTheLock) { /* Handled */ } else mainWindow?.show(); }); // Handle terminal termination (Ctrl+C / SIGTERM) const handleSignal = (signal: string) => { - if (!isQuitting && mainWindow) { mainWindow.hide(); } - else if (isQuitting) { app.quit(); } + console.log(`[App] Received ${signal}`); + if (!(app as any).isQuitting && mainWindow) { + console.log(`[App] Hiding window instead of quitting due to ${signal}`); + mainWindow.hide(); + } + else if ((app as any).isQuitting) { app.quit(); } }; process.on('SIGINT', () => handleSignal('SIGINT')); process.on('SIGTERM', () => handleSignal('SIGTERM')); @@ -472,7 +529,7 @@ ipcMain.handle('admin-delete-user', async (event, userId: string) => { initBacke ipcMain.handle('admin-get-accounts', async () => { initBackend(); return backend ? await backend.getAdminAccounts() : []; }); ipcMain.handle('admin-remove-account', async (event, steamId: string) => { initBackend(); if (backend) await backend.forceRemoveAccount(steamId); return true; }); ipcMain.handle('force-sync', async () => { await syncAccounts(true); return true; }); -ipcMain.handle('update-app-icon', (event, themeName: string) => { /* Icon handled by setupTray/createWindow */ return true; }); +ipcMain.handle('update-app-icon', (event, themeName: string) => { setAppIcon(themeName); return true; }); ipcMain.handle('switch-account', async (event, loginName: string) => { if (!loginName) return false; try {