Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions HYPOTHESES.md
Original file line number Diff line number Diff line change
@@ -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.
156 changes: 95 additions & 61 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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: [],
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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) {}
});

Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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()

Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
29 changes: 18 additions & 11 deletions src/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,21 +213,28 @@ export const Settings = () => {
<div className="space-y-0.5">
<Label htmlFor="launchAtStartup" className="text-base font-medium flex items-center gap-2">
<Power className="w-4 h-4 text-muted-foreground" />
Auto-Launch
Start on System Login
</Label>
<p className="text-sm text-muted-foreground">
Start automatically on login.
Automatically launch Git Watcher Pro when you sign in.
</p>
</div>
<Checkbox
id="launchAtStartup"
className="h-6 w-6 border-2 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground shadow-sm hover:shadow-md transition-all"
checked={launchAtStartup}
onCheckedChange={(checked) => {
setLaunchAtStartup(checked === true);
handleSave({ launchAtStartup: checked === true });
}}
/>
<div className="flex items-center gap-2">
{launchAtStartup && (
<span className="text-xs font-medium text-green-600 bg-green-500/10 px-2 py-0.5 rounded animate-in fade-in">
Enabled
</span>
)}
<Checkbox
id="launchAtStartup"
className="h-6 w-6 border-2 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground shadow-sm hover:shadow-md transition-all"
checked={launchAtStartup}
onCheckedChange={(checked) => {
setLaunchAtStartup(checked === true);
handleSave({ launchAtStartup: checked === true });
}}
/>
</div>
</div>

{/* Row 3: Minimize */}
Expand Down
Binary file removed verification/app_screenshot.png
Binary file not shown.
Binary file removed verification/dashboard.png
Binary file not shown.
Binary file removed verification/logs.png
Binary file not shown.
9 changes: 0 additions & 9 deletions verification/screenshot.py

This file was deleted.

Binary file removed verification/settings.png
Binary file not shown.
36 changes: 0 additions & 36 deletions verification/verify_changes.py

This file was deleted.