diff --git a/HYPOTHESES.md b/HYPOTHESES.md new file mode 100644 index 0000000..72deeb9 --- /dev/null +++ b/HYPOTHESES.md @@ -0,0 +1,29 @@ +# Top 20 Hypotheses for "Black Screen" in Portable Electron App + +Ranked by probability based on the user's logs and environment. + +## High Probability (The "Smoking Guns") +1. **Missing Frontend Assets (95%):** The `dist/` folder was listed in `.gitignore` but not explicitly included in `package.json`'s `"files"`. Electron-builder defaults to respecting `.gitignore`, causing the build to contain only the backend (`dist-electron`) and no frontend. `loadFile` fails -> Chrome error page. +2. **Incorrect `loadFile` Path Resolution (60%):** Portable apps run from a temp directory (e.g., `%TEMP%/GitWatcher...`). The `app.getAppPath()` might return a path ending in `app.asar`, and if the `path.join` logic isn't robust, it points to a non-existent file. +3. **ASAR Unpacking Failure (40%):** If `asar` is enabled (default), `index.html` is inside an archive. Sometimes portable builds fail to read from the archive if the path format isn't perfect (e.g., mixing `\` and `/`). +4. **Security Policy Blocking `file://` (35%):** Browsers (Chromium) restrict loading local resources from `file://` URLs for security. While Electron usually allows this, specific portable environments might trigger stricter sandboxing. +5. **Vite Base Path Mismatch (30%):** `vite.config.ts` sets `base: './'`. If the app is loaded from a root context or the `index.html` expects assets at relative paths that don't match the `file://` structure, JS/CSS fails to load (though usually this leaves a white screen, not a Chrome error page). + +## Medium Probability (Plausible) +6. **Temp Directory Cleanup (25%):** Antivirus or system cleaners might aggressively delete files from `%TEMP%` while the app is running, removing `index.html` before it loads. +7. **MIME Type Registry Issues (20%):** On some Windows machines, the registry entries for `.js` or `.html` content types are broken. Electron relies on the OS for file association MIME types when serving via `file://`. +8. **Context Isolation Crash (15%):** If the `preload` script crashes due to a missing dependency (only in production), the renderer process might terminate or fail to initialize the React root. +9. **Case Sensitivity (15%):** Windows is case-insensitive, but if the internal ASAR reader is case-sensitive and the code requests `Index.html` vs `index.html`, it fails. +10. **Race Condition (Window vs Load) (10%):** The window is shown before the `loadFile` promise completes or fails, leaving a blank state if the failure isn't handled gracefully. +11. **`electron-router` / History API (10%):** If the app uses `react-router` with `browserHistory`, it fails on `file://` because there is no server to handle routes. (This app seems to use memory/hash routing or simple state, so less likely). +12. **CSP (Content Security Policy) (10%):** A strict CSP meta tag or header might be blocking script execution in the production build. + +## Low Probability (Unlikely but Possible) +13. **GPU Process Crash (5%):** Chromium GPU acceleration failing on specific hardware (e.g., VMs, old drivers) causing a rendering failure (black screen). +14. **Node Integration disabled but required (5%):** If the renderer tries to use `require` and `nodeIntegration` is false (correctly), but the code wasn't refactored for the preload bridge. +15. **Memory Limit (2%):** The portable executable extraction fills the RAM disk or temp space, causing incomplete extraction. +16. **Port Conflict (1%):** (Relevant only if not using `file://` but a local server). The user said no ports, but if `vite preview` was somehow involved in the build chain (unlikely). +17. **Corrupted Electron Binary (1%):** The `electron-builder` download was corrupted during the build process. +18. **Bit Defender / Firewall (1%):** Blocking the application from "phoning home" or even loading local resources if it deems the temp file suspicious. +19. **React Hydration Error (1%):** React 18 strict mode causing a crash on mount that unmounts the root. (Usually leaves white screen, not chrome error). +20. **Cosmic Rays (<1%):** Bit flip in the `index.html` file path string. diff --git a/electron/main.ts b/electron/main.ts index afa1c01..e9aa68c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,8 +1,9 @@ -import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, shell, clipboard, dialog, globalShortcut } from 'electron' +import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, shell, clipboard, dialog, globalShortcut, protocol, net } from 'electron' import { join, basename } from 'node:path' import { existsSync, writeFileSync } from 'node:fs' import { exec } from 'node:child_process' import { webcrypto } from 'node:crypto' +import { pathToFileURL } from 'node:url' import simpleGit from 'simple-git' import notifier from 'node-notifier' import Store from 'electron-store' @@ -19,6 +20,11 @@ const ROOT_PATH = app.getAppPath() const DIST_PATH = join(ROOT_PATH, 'dist') const PRELOAD_PATH = join(ROOT_PATH, 'dist-electron/preload.cjs') +// Register protocol before app ready +protocol.registerSchemesAsPrivileged([ + { scheme: 'app', privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, stream: true } } +]) + const store = new Store({ defaults: { repos: [], @@ -69,67 +75,78 @@ async function checkRepos() { let reposUpdated = false; - for (const repo of repos) { - try { - const git = simpleGit(repo.path) - - // Auto-detect remote URL if missing - if (!repo.githubUrl) { - const remotes = await git.getRemotes(true); - const origin = remotes.find(r => r.name === 'origin') || remotes[0]; - if (origin) { - repo.githubUrl = convertGitUrlToHttps(origin.refs.fetch); - reposUpdated = true; + // Prevent concurrent checks to avoid git lock issues + if ((global as any).isCheckingRepos) { + console.log('Skipping checkRepos: previous check still in progress'); + return; + } + (global as any).isCheckingRepos = true; + + try { + for (const repo of repos) { + try { + const git = simpleGit(repo.path) + + // Auto-detect remote URL if missing + if (!repo.githubUrl) { + const remotes = await git.getRemotes(true); + const origin = remotes.find(r => r.name === 'origin') || remotes[0]; + if (origin) { + repo.githubUrl = convertGitUrlToHttps(origin.refs.fetch); + reposUpdated = true; + } } - } - await git.fetch() - const status = await git.status() - - const updateData: any = { - id: repo.id, - status: 'clean', - commitsBehind: 0, - commitsAhead: 0, - githubUrl: repo.githubUrl - }; - - if (status.isClean() === false) { - updateData.status = 'dirty'; - } else if (status.ahead > 0 && status.behind > 0) { - updateData.status = 'diverged'; - updateData.commitsBehind = status.behind; - updateData.commitsAhead = status.ahead; - } else if (status.behind > 0) { - updateData.status = 'behind'; - updateData.commitsBehind = status.behind; - - if (repo.autoPull) { - await git.pull() - notifyUser(repo.name, `Pulled ${status.behind} new commits.`) - updateData.status = 'clean'; - updateData.commitsBehind = 0; - } else { - notifyUser(repo.name, `${status.behind} commits waiting.`, true) + await git.fetch() + const status = await git.status() + + const updateData: any = { + id: repo.id, + status: 'clean', + commitsBehind: 0, + commitsAhead: 0, + githubUrl: repo.githubUrl + }; + + if (status.isClean() === false) { + updateData.status = 'dirty'; + } else if (status.ahead > 0 && status.behind > 0) { + updateData.status = 'diverged'; + updateData.commitsBehind = status.behind; + updateData.commitsAhead = status.ahead; + } else if (status.behind > 0) { + updateData.status = 'behind'; + updateData.commitsBehind = status.behind; + + if (repo.autoPull) { + await git.pull() + notifyUser(repo.name, `Pulled ${status.behind} new commits.`) + updateData.status = 'clean'; + updateData.commitsBehind = 0; + } else { + notifyUser(repo.name, `${status.behind} commits waiting.`, true) + } + } else if (status.ahead > 0) { + updateData.status = 'ahead'; + updateData.commitsAhead = status.ahead; + } + + if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('repo-update', updateData) + + } catch (err: any) { + console.error(`Error checking ${repo.name}:`, err) + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('repo-error', { id: repo.id, message: err.message || 'Unknown error' }) + mainWindow.webContents.send('repo-update', { id: repo.id, status: 'error' }) } - } else if (status.ahead > 0) { - updateData.status = 'ahead'; - updateData.commitsAhead = status.ahead; - } - - if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('repo-update', updateData) - - } catch (err: any) { - console.error(`Error checking ${repo.name}:`, err) - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('repo-error', { id: repo.id, message: err.message || 'Unknown error' }) - mainWindow.webContents.send('repo-update', { id: repo.id, status: 'error' }) } } - } - if (reposUpdated) { - store.set('repos', repos); + if (reposUpdated) { + store.set('repos', repos); + } + } finally { + (global as any).isCheckingRepos = false; } } @@ -218,15 +235,18 @@ function createWindow(): void { sandbox: false, nodeIntegration: false, contextIsolation: true, - devTools: true + devTools: true, + webSecurity: app.isPackaged ? false : true, // Relax security for portable builds to avoid "Not allowed to load local resource" + allowRunningInsecureContent: true } }) // LOGGING DIAGNOSTICS - mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => { - console.error('FAILED TO LOAD:', errorCode, errorDescription); + mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => { + const msg = `FAILED TO LOAD: ${errorCode} - ${errorDescription} - URL: ${validatedURL}`; + console.error(msg); try { - writeFileSync(join(app.getPath('userData'), 'load-error.log'), `Error: ${errorCode} - ${errorDescription}`); + writeFileSync(join(app.getPath('userData'), 'load-error.log'), msg); } catch (e) {} }); @@ -255,7 +275,12 @@ function createWindow(): void { if (process.env.VITE_DEV_SERVER_URL) { mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL) } else { - mainWindow.loadFile(join(DIST_PATH, 'index.html')) + // "Radical" Approach: Use a custom protocol to serve files. + // This bypasses file:// restrictions and path issues in portable builds. + mainWindow.loadURL('app://git-watcher/index.html').catch(e => { + console.error('Failed to load app:// URL:', e); + dialog.showErrorBox('Application Load Error', `Failed to load application resources.\n\nError: ${e.message}\n\nPlease verify the installation.`); + }); } mainWindow.webContents.setWindowOpenHandler(({ url }) => { @@ -302,6 +327,15 @@ app.whenReady().then(() => { updateLoginSettings(); + // Handle custom protocol for robust asset serving + protocol.handle('app', (request) => { + const url = request.url.slice('app://git-watcher/'.length) + const filePath = join(DIST_PATH, url) + + // Serve file using net module (safe, handles asar automatically) + return net.fetch(pathToFileURL(filePath).toString()) + }) + createWindow() createTray() diff --git a/package.json b/package.json index 3fedba6..312716d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,16 @@ "build": { "appId": "com.gitwatcher.app", "productName": "Git Watcher Pro", + "files": [ + "dist-electron", + "dist", + "electron/assets", + "!**/*.map", + "!**/*.d.ts", + "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}" + ], "win": { + "icon": "electron/assets/icon.png", "target": [ "nsis", "portable" diff --git a/src/Settings.tsx b/src/Settings.tsx index 2b9d5fd..98a328a 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -213,21 +213,28 @@ export const Settings = () => {
- Start automatically on login. + Automatically launch Git Watcher Pro when you sign in.