Browse Source

fix: Improve Electron application stability and asset handling

- Completely rewrite main.js for reliable asset loading
- Update preload.js with proper security context isolation
- Fix file:// protocol handling for application resources
- Add proper error logging and reporting in Electron context
- Disable service workers in Electron environment
- Fix path resolution for assets in packaged application
- Add prerequisite checking for Electron builds
- Update electron-builder configuration

The changes resolve persistent issues with:
1. Missing assets in packaged application
2. Incorrect path resolution in production builds
3. Service worker conflicts in desktop environment
4. Security context handling in preload script
5. Electron build process reliability
electron_fix_20250317
Matthew Raymer 2 weeks ago
parent
commit
4230deab1d
  1. 36
      electron-builder.json
  2. 68
      package-lock.json
  3. 52
      package.json
  4. 6
      scripts/build-electron.js
  5. 177
      scripts/check-electron-prerequisites.js
  6. 61
      scripts/fix-electron-paths.js
  7. 14
      scripts/notarize.js
  8. 33
      src/electron/electron-logger.js
  9. 327
      src/electron/main.js
  10. 134
      src/electron/preload.js
  11. 13
      src/registerServiceWorker.ts
  12. 1
      test-scripts/requirements.txt
  13. 21
      vite.config.electron.mts

36
electron-builder.json

@ -0,0 +1,36 @@
{
"appId": "app.timesafari.app",
"productName": "TimeSafari",
"directories": {
"output": "dist-electron-packages",
"buildResources": "build"
},
"files": [
"dist-electron/**/*",
"node_modules/**/*",
"package.json",
"src/electron/electron-logger.js"
],
"extraResources": [
{
"from": "src/utils",
"to": "utils",
"filter": ["**/*"]
}
],
"extraMetadata": {
"main": "src/electron/main.js"
},
"linux": {
"target": ["AppImage"],
"category": "Utility",
"maintainer": "TimeSafari Team"
},
"mac": {
"target": ["dmg"],
"category": "public.app-category.productivity"
},
"win": {
"target": ["nsis"]
}
}

68
package-lock.json

@ -4666,12 +4666,6 @@
"scrypt-js": "3.0.1" "scrypt-js": "3.0.1"
} }
}, },
"node_modules/@ethersproject/json-wallets/node_modules/aes-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz",
"integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==",
"license": "MIT"
},
"node_modules/@ethersproject/keccak256": { "node_modules/@ethersproject/keccak256": {
"version": "5.8.0", "version": "5.8.0",
"resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz",
@ -5004,9 +4998,9 @@
} }
}, },
"node_modules/@expo/cli": { "node_modules/@expo/cli": {
"version": "0.22.19", "version": "0.22.20",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.19.tgz", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.20.tgz",
"integrity": "sha512-vgAM3gUsAUQWgDm10RIYSrWQ5q235Ir/lMUdx5Yd/gObYaDlaBIdsq5H72eJ44QB+4ndvhm2wbqntTI19kBIRw==", "integrity": "sha512-BU2ASlw0Gaj3ou/TxVsgvzK+XK8Z14Yq3mmLyvMcMAQrdExZLNmvMZ3A3x6q2uMgSJM3aoQBUuVXS/Ny+lYgDA==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true, "peer": true,
@ -10391,9 +10385,9 @@
} }
}, },
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "5.2.1", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.2.tgz",
"integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", "integrity": "sha512-IY0aPonWZI2huxrWjoSBUQX14GThitmr1sc2OUJymcgnY5RlUI7HoXGAnFEoVNRsck/kS6inGvxCN6CoHu86yQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -10913,9 +10907,9 @@
} }
}, },
"node_modules/aes-js": { "node_modules/aes-js": {
"version": "4.0.0-beta.5", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz",
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/agent-base": { "node_modules/agent-base": {
@ -12594,9 +12588,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001704", "version": "1.0.30001705",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz",
"integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==", "integrity": "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==",
"devOptional": true, "devOptional": true,
"funding": [ "funding": [
{ {
@ -14518,9 +14512,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.118", "version": "1.5.119",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.118.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.119.tgz",
"integrity": "sha512-yNDUus0iultYyVoEFLnQeei7LOQkL8wg8GQpkPCRrOlJXlcCwa6eGKZkxQ9ciHsqZyYbj8Jd94X1CTPzGm+uIA==", "integrity": "sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==",
"devOptional": true, "devOptional": true,
"license": "ISC" "license": "ISC"
}, },
@ -15303,6 +15297,12 @@
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
} }
}, },
"node_modules/ethers/node_modules/aes-js": {
"version": "4.0.0-beta.5",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
"license": "MIT"
},
"node_modules/ethers/node_modules/tslib": { "node_modules/ethers/node_modules/tslib": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
@ -15554,15 +15554,15 @@
} }
}, },
"node_modules/expo": { "node_modules/expo": {
"version": "52.0.38", "version": "52.0.39",
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.38.tgz", "resolved": "https://registry.npmjs.org/expo/-/expo-52.0.39.tgz",
"integrity": "sha512-6DZJjN/oEeYOPGoNUWE41vUuwVSl/Cg9o3rTbP62Pchgspp61Elsf8G7FtdcAtdgOzkJmbnPrPqklpMXGwpgfA==", "integrity": "sha512-EOnrgj8MHSt0o0SIBhM7jCim2QpJJNonbSATn9LqNtVgKtotIg718G/OrP5/g0GUAOBDyxHH9PfNu/aq9c0vDw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"@expo/cli": "0.22.19", "@expo/cli": "0.22.20",
"@expo/config": "~10.0.11", "@expo/config": "~10.0.11",
"@expo/config-plugins": "~9.0.17", "@expo/config-plugins": "~9.0.17",
"@expo/fingerprint": "0.11.11", "@expo/fingerprint": "0.11.11",
@ -16242,9 +16242,9 @@
"peer": true "peer": true
}, },
"node_modules/flow-parser": { "node_modules/flow-parser": {
"version": "0.265.0", "version": "0.265.2",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.265.0.tgz", "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.265.2.tgz",
"integrity": "sha512-C+bg/TZsDVlLMF14+q9P9FB2pjQSgWwYs0pkIMPE1FsZWS4A0kk1M28V6YphpxAPr3AISVRZ6VgpDepvCk6dGw==", "integrity": "sha512-DX2mp5u3lNJHl5dH8R1KrcrDsiJC02zFcG95p4b0YcDCzZZW+v9za2Csv5bQ0cq4jNzGx0gFU9jFZyM7FOyNFw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true, "peer": true,
@ -21378,9 +21378,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.9", "version": "3.3.10",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz",
"integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -21729,9 +21729,9 @@
} }
}, },
"node_modules/nostr-tools": { "node_modules/nostr-tools": {
"version": "2.10.4", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.11.0.tgz",
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==", "integrity": "sha512-kRtXI9j5f45NvIcdJacQ0UEAfEb7p/jhZqhAGLQWtUd5idZJPYdSyR8hdw+MmpGH4TCMH5plZrXzFltIIZrkEA==",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@noble/ciphers": "^0.5.1", "@noble/ciphers": "^0.5.1",

52
package.json

@ -23,14 +23,14 @@
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)", "check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
"clean:electron": "rimraf dist-electron", "clean:electron": "rimraf dist-electron",
"build:pywebview": "vite build --config vite.config.pywebview.mts", "build:pywebview": "vite build --config vite.config.pywebview.mts",
"build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js", "build:electron": "npm run check:electron && npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
"build:capacitor": "vite build --config vite.config.capacitor.mts", "build:capacitor": "vite build --config vite.config.capacitor.mts",
"build:web": "vite build --config vite.config.web.mts", "build:web": "vite build --config vite.config.web.mts",
"electron:dev": "npm run build && electron dist-electron", "electron:dev": "concurrently \"vite --config vite.config.electron.mts\" \"electron .\"",
"electron:start": "electron dist-electron", "electron:start": "electron dist-electron",
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage", "electron:build-linux": "npm run check:electron && npm run build:electron && electron-builder --linux AppImage",
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb", "electron:build-linux-deb": "npm run check:electron && npm run build:electron && electron-builder --linux deb",
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage", "electron:build-linux-prod": "NODE_ENV=production npm run check:electron &&npm run build:electron && electron-builder --linux AppImage",
"build:electron-prod": "NODE_ENV=production npm run build:electron", "build:electron-prod": "NODE_ENV=production npm run build:electron",
"pywebview:dev": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py", "pywebview:dev": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
"pywebview:build": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py", "pywebview:build": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
@ -40,7 +40,10 @@
"fastlane:ios:beta": "cd ios && fastlane beta", "fastlane:ios:beta": "cd ios && fastlane beta",
"fastlane:ios:release": "cd ios && fastlane release", "fastlane:ios:release": "cd ios && fastlane release",
"fastlane:android:beta": "cd android && fastlane beta", "fastlane:android:beta": "cd android && fastlane beta",
"fastlane:android:release": "cd android && fastlane release" "fastlane:android:release": "cd android && fastlane release",
"check:electron": "node scripts/check-electron-prerequisites.js",
"electron:build": "npm run check:electron && vite build --config vite.config.electron.mts && node scripts/fix-electron-paths.js && electron-builder",
"postinstall": "electron-builder install-app-deps"
}, },
"dependencies": { "dependencies": {
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
@ -157,28 +160,31 @@
"build": { "build": {
"appId": "app.timesafari", "appId": "app.timesafari",
"productName": "TimeSafari", "productName": "TimeSafari",
"directories": {
"output": "dist-electron-packages"
},
"files": [ "files": [
"dist-electron/**/*", "dist-electron/**/*",
"src/electron/**/*", "!dist-electron/node_modules/**/*"
"main.js"
], ],
"extraResources": [ "directories": {
{ "output": "dist-electron-packages",
"from": "dist-electron", "buildResources": "build-resources"
"to": "." },
} "extraResources": [],
"asar": true,
"asarUnpack": [
"dist-electron/www/assets/**/*"
], ],
"linux": { "linux": {
"target": [ "target": ["AppImage"],
"AppImage", "category": "Utility",
"deb" "executableName": "TimeSafari"
], },
"category": "Office", "mac": {
"icon": "build/icon.png" "category": "public.app-category.productivity"
},
"win": {
"target": ["nsis"]
}, },
"asar": true "artifactName": "TimeSafari-${version}-${arch}.${ext}",
"publish": null
} }
} }

6
scripts/build-electron.js

@ -88,6 +88,12 @@ async function main() {
throw new Error('package.json not found in build directory'); throw new Error('package.json not found in build directory');
} }
// Copy the electron-logger.js file
const loggerSrc = path.join(__dirname, '../src/electron/electron-logger.js');
const loggerDest = path.join(distElectronDir, 'electron-logger.js');
fs.copyFileSync(loggerSrc, loggerDest);
console.log(`Copying src/electron/electron-logger.js to ${loggerDest}`);
console.log('Build completed successfully!'); console.log('Build completed successfully!');
} catch (error) { } catch (error) {
console.error('Build failed:', error); console.error('Build failed:', error);

177
scripts/check-electron-prerequisites.js

@ -0,0 +1,177 @@
#!/usr/bin/env node
/**
* @file check-electron-prerequisites.js
* @description Verifies and installs required dependencies for Electron builds
*
* This script checks if Python's distutils module is available, which is required
* by node-gyp when compiling native Node.js modules during Electron packaging.
* Without distutils, builds will fail with "ModuleNotFoundError: No module named 'distutils'".
*
* The script performs the following actions:
* 1. Checks if Python's distutils module is available
* 2. If missing, offers to install setuptools package which provides distutils
* 3. Attempts installation through pip or pip3
* 4. Provides manual installation instructions if automated installation fails
*
* Usage:
* - Direct execution: node scripts/check-electron-prerequisites.js
* - As npm script: npm run check:electron
* - Before builds: npm run check:electron && electron-builder
*
* Exit codes:
* - 0: All prerequisites are met or were successfully installed
* - 1: Prerequisites are missing and weren't installed
*
* @author [YOUR_NAME]
* @version 1.0.0
* @license MIT
*/
const { execSync } = require('child_process');
const readline = require('readline');
const chalk = require('chalk'); // You might need to add this to your dependencies
console.log(chalk.blue('🔍 Checking Electron build prerequisites...'));
/**
* Checks if Python's distutils module is available
*
* This function attempts to import the distutils module in Python.
* If successful, it means node-gyp will be able to compile native modules.
* If unsuccessful, the Electron build will likely fail when compiling native dependencies.
*
* @returns {boolean} True if distutils is available, false otherwise
*
* @example
* if (checkDistutils()) {
* console.log('Ready to build Electron app');
* }
*/
function checkDistutils() {
try {
// Attempt to import distutils using Python
// We use stdio: 'ignore' to suppress any Python output
execSync('python -c "import distutils"', { stdio: 'ignore' });
console.log(chalk.green('✅ Python distutils is available'));
return true;
} catch (e) {
// This error occurs if either Python is not found or if distutils is missing
console.log(chalk.red('❌ Python distutils is missing'));
return false;
}
}
/**
* Installs the setuptools package which provides distutils
*
* This function attempts to install setuptools using pip or pip3.
* Setuptools is a package that provides the distutils module needed by node-gyp.
* In Python 3.12+, distutils was moved out of the standard library into setuptools.
*
* The function tries multiple installation methods:
* 1. First attempts with pip
* 2. If that fails, tries with pip3
* 3. If both fail, provides instructions for manual installation
*
* @returns {Promise<boolean>} True if installation succeeded, false otherwise
*
* @example
* const success = await installSetuptools();
* if (success) {
* console.log('Ready to proceed with build');
* } else {
* console.log('Please fix prerequisites manually');
* }
*/
async function installSetuptools() {
console.log(chalk.yellow('📦 Attempting to install setuptools...'));
try {
// First try with pip, commonly used on all platforms
execSync('pip install setuptools', { stdio: 'inherit' });
console.log(chalk.green('✅ Successfully installed setuptools'));
return true;
} catch (pipError) {
try {
// If pip fails, try with pip3 (common on Linux distributions)
console.log(chalk.yellow('⚠️ Trying with pip3...'));
execSync('pip3 install setuptools', { stdio: 'inherit' });
console.log(chalk.green('✅ Successfully installed setuptools using pip3'));
return true;
} catch (pip3Error) {
// If both methods fail, provide manual installation guidance
console.log(chalk.red('❌ Failed to install setuptools automatically'));
console.log(chalk.yellow('📝 Please install it manually with:'));
console.log(' pip install setuptools');
console.log(' or');
console.log(' sudo apt install python3-setuptools (on Debian/Ubuntu)');
console.log(' sudo pacman -S python-setuptools (on Arch Linux)');
console.log(' sudo dnf install python3-setuptools (on Fedora)');
console.log(' brew install python-setuptools (on macOS with Homebrew)');
return false;
}
}
}
/**
* Main execution function
*
* This function orchestrates the checking and installation process:
* 1. Checks if distutils is already available
* 2. If not, informs the user and prompts for installation
* 3. Based on user input, attempts to install or exits
*
* The function handles interactive user prompts and orchestrates
* the overall flow of the script.
*
* @returns {Promise<void>}
* @throws Will exit process with code 1 if prerequisites aren't met
*/
async function main() {
// First check if distutils is already available
if (checkDistutils()) {
// All prerequisites are met, exit successfully
process.exit(0);
}
// Inform the user about the missing prerequisite
console.log(chalk.yellow('⚠️ Python distutils is required for Electron builds'));
console.log(chalk.yellow('⚠️ This is needed to compile native modules during the build process'));
// Set up readline interface for user interaction
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Prompt the user for installation permission
const answer = await new Promise(resolve => {
rl.question(chalk.blue('Would you like to install setuptools now? (y/n) '), resolve);
});
// Clean up readline interface
rl.close();
if (answer.toLowerCase() === 'y') {
// User agreed to installation
const success = await installSetuptools();
if (success) {
// Installation succeeded, exit successfully
process.exit(0);
} else {
// Installation failed, exit with error
process.exit(1);
}
} else {
// User declined installation
console.log(chalk.yellow('⚠️ Build may fail without distutils'));
process.exit(1);
}
}
// Execute the main function and handle any uncaught errors
main().catch(error => {
console.error(chalk.red('Error during prerequisites check:'), error);
process.exit(1);
});

61
scripts/fix-electron-paths.js

@ -0,0 +1,61 @@
/**
* Fix path resolution issues in the Electron build
*/
const fs = require('fs');
const path = require('path');
const glob = require('glob');
// Fix asset paths in HTML file
function fixHtmlPaths() {
const htmlFile = path.join(__dirname, '../dist-electron/index.html');
if (fs.existsSync(htmlFile)) {
let html = fs.readFileSync(htmlFile, 'utf8');
// Convert absolute paths to relative
html = html.replace(/src="\//g, 'src="./');
html = html.replace(/href="\//g, 'href="./');
fs.writeFileSync(htmlFile, html);
console.log('✅ Fixed paths in index.html');
}
}
// Fix asset imports in JS files
function fixJsPaths() {
const jsFiles = glob.sync('dist-electron/assets/*.js');
jsFiles.forEach(file => {
let content = fs.readFileSync(file, 'utf8');
// Replace absolute imports with relative ones
const originalContent = content;
content = content.replace(/["']\/assets\//g, '"./assets/');
if (content !== originalContent) {
fs.writeFileSync(file, content);
console.log(`✅ Fixed paths in ${path.basename(file)}`);
}
});
}
// Add base href to HTML
function addBaseHref() {
const htmlFile = path.join(__dirname, '../dist-electron/index.html');
if (fs.existsSync(htmlFile)) {
let html = fs.readFileSync(htmlFile, 'utf8');
// Add base href if not present
if (!html.includes('<base href=')) {
html = html.replace('</head>', '<base href="./">\n</head>');
fs.writeFileSync(htmlFile, html);
console.log('✅ Added base href to index.html');
}
}
}
// Run all fixes
fixHtmlPaths();
fixJsPaths();
addBaseHref();
console.log('🎉 Electron path fixes completed');

14
scripts/notarize.js

@ -0,0 +1,14 @@
// This is a placeholder notarize script that does nothing for non-macOS platforms
// Only necessary for macOS app store submissions
exports.default = async function notarizing(context) {
// Only notarize macOS builds
if (context.electronPlatformName !== 'darwin') {
console.log('Skipping notarization for non-macOS platform');
return;
}
// For macOS, we would implement actual notarization here
console.log('This is where macOS notarization would happen');
// We're just returning with no action for non-macOS builds
};

33
src/electron/electron-logger.js

@ -0,0 +1,33 @@
/**
* Electron-specific logger implementation
*/
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
// Create logs directory if it doesn't exist
const logsDir = path.join(app.getPath('userData'), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const logFile = path.join(logsDir, `electron-${new Date().toISOString().split('T')[0]}.log`);
function log(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
// Write to log file
fs.appendFileSync(logFile, logMessage);
// Also output to console
console[level.toLowerCase()](message);
}
module.exports = {
info: (message) => log('INFO', message),
warn: (message) => log('WARN', message),
error: (message) => log('ERROR', message),
debug: (message) => log('DEBUG', message),
getLogPath: () => logFile
};

327
src/electron/main.js

@ -1,174 +1,217 @@
const { app, BrowserWindow } = require("electron"); const { app, BrowserWindow, session, protocol, ipcMain, dialog } = require('electron');
const path = require("path"); const path = require('path');
const fs = require("fs"); const fs = require('fs');
const logger = require("../utils/logger"); const url = require('url');
// Check if running in dev mode // Global window reference
const isDev = process.argv.includes("--inspect"); let mainWindow = null;
// Debug flags
const isDev = !app.isPackaged;
// Helper for logging
function logDebug(...args) {
console.log('[DEBUG]', ...args);
}
function logError(...args) {
console.error('[ERROR]', ...args);
if (!isDev && mainWindow) {
dialog.showErrorBox('TimeSafari Error', args.join(' '));
}
}
// Get the most appropriate app path
function getAppPath() {
if (app.isPackaged) {
const possiblePaths = [
path.join(process.resourcesPath, 'app.asar', 'dist-electron'),
path.join(process.resourcesPath, 'app.asar'),
path.join(process.resourcesPath, 'app'),
app.getAppPath()
];
for (const testPath of possiblePaths) {
const testFile = path.join(testPath, 'www', 'index.html');
if (fs.existsSync(testFile)) {
logDebug(`Found valid app path: ${testPath}`);
return testPath;
}
}
logError('Could not find valid app path');
return path.join(process.resourcesPath, 'app.asar'); // Default fallback
} else {
return __dirname;
}
}
// Create the browser window
function createWindow() { function createWindow() {
// Add before createWindow function logDebug('Creating window with paths:');
const preloadPath = path.join(__dirname, "preload.js"); logDebug('- process.resourcesPath:', process.resourcesPath);
logger.log("Checking preload path:", preloadPath); logDebug('- app.getAppPath():', app.getAppPath());
logger.log("Preload exists:", fs.existsSync(preloadPath)); logDebug('- __dirname:', __dirname);
// Create the browser window. // Create the browser window
const mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
webPreferences: { webPreferences: {
nodeIntegration: false, preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, contextIsolation: true,
webSecurity: true, nodeIntegration: false,
allowRunningInsecureContent: false, webSecurity: true
preload: path.join(__dirname, "preload.js"), }
},
}); });
// Always open DevTools for now // Fix root file paths - replaces all protocol handling
mainWindow.webContents.openDevTools(); protocol.interceptFileProtocol('file', (request, callback) => {
let urlPath = request.url.substr(7); // Remove 'file://' prefix
// Intercept requests to fix asset paths urlPath = decodeURIComponent(urlPath); // Handle special characters
mainWindow.webContents.session.webRequest.onBeforeRequest(
{ // Debug all asset requests
urls: [ if (urlPath.includes('assets/') || urlPath.endsWith('.js') || urlPath.endsWith('.css') || urlPath.endsWith('.html')) {
"file://*/*/assets/*", logDebug(`Intercepted request for: ${urlPath}`);
"file://*/assets/*", }
"file:///assets/*", // Catch absolute paths
"<all_urls>", // Catch all URLs as a fallback // Fix paths for files at root like registerSW.js or manifest.webmanifest
], if (urlPath.endsWith('registerSW.js') ||
}, urlPath.endsWith('manifest.webmanifest') ||
(details, callback) => { urlPath.endsWith('sw.js')) {
let url = details.url;
const appBasePath = getAppPath();
// Handle paths that don't start with file:// const filePath = path.join(appBasePath, 'www', path.basename(urlPath));
if (!url.startsWith("file://") && url.includes("/assets/")) {
url = `file://${path.join(__dirname, "www", url)}`; if (fs.existsSync(filePath)) {
logDebug(`Serving ${urlPath} from ${filePath}`);
return callback({ path: filePath });
} else {
// For service worker, provide empty content to avoid errors
if (urlPath.endsWith('registerSW.js') || urlPath.endsWith('sw.js')) {
logDebug(`Providing empty SW file for ${urlPath}`);
// Create an empty JS file content that does nothing
const tempFile = path.join(app.getPath('temp'), path.basename(urlPath));
fs.writeFileSync(tempFile, '// Service workers disabled in Electron\n');
return callback({ path: tempFile });
}
} }
}
// Handle absolute paths starting with /assets/
if (url.includes("/assets/") && !url.includes("/www/assets/")) { // Handle assets paths that might be requested from root
const baseDir = url.includes("dist-electron") if (urlPath.startsWith('/assets/') || urlPath === '/assets') {
? url.substring( const appBasePath = getAppPath();
0, const filePath = path.join(appBasePath, 'www', urlPath);
url.indexOf("/dist-electron") + "/dist-electron".length, logDebug(`Redirecting ${urlPath} to ${filePath}`);
) return callback({ path: filePath });
: `file://${__dirname}`; }
const assetPath = url.split("/assets/")[1];
const newUrl = `${baseDir}/www/assets/${assetPath}`; // Handle assets paths that are missing the www folder
callback({ redirectURL: newUrl }); if (urlPath.includes('/assets/')) {
return; const appBasePath = getAppPath();
const relativePath = urlPath.substring(urlPath.indexOf('/assets/'));
const filePath = path.join(appBasePath, 'www', relativePath);
if (fs.existsSync(filePath)) {
logDebug(`Fixing asset path ${urlPath} to ${filePath}`);
return callback({ path: filePath });
} }
}
callback({}); // No redirect for other URLs // For all other paths, just pass them through
}, callback({ path: urlPath });
); });
if (isDev) {
// Debug info
logger.log("Debug Info:");
logger.log("Running in dev mode:", isDev);
logger.log("App is packaged:", app.isPackaged);
logger.log("Process resource path:", process.resourcesPath);
logger.log("App path:", app.getAppPath());
logger.log("__dirname:", __dirname);
logger.log("process.cwd():", process.cwd());
}
const indexPath = path.join(__dirname, "www", "index.html");
if (isDev) {
logger.log("Loading index from:", indexPath);
logger.log("www path:", path.join(__dirname, "www"));
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
}
if (!fs.existsSync(indexPath)) {
logger.error(`Index file not found at: ${indexPath}`);
throw new Error("Index file not found");
}
// Add CSP headers to allow API connections // Set up CSP headers - more permissive in dev mode
mainWindow.webContents.session.webRequest.onHeadersReceived( session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
(details, callback) => { callback({
callback({ responseHeaders: {
responseHeaders: { ...details.responseHeaders,
...details.responseHeaders, 'Content-Security-Policy': [
"Content-Security-Policy": [ isDev
"default-src 'self';" + ? "default-src 'self' file:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://*; connect-src 'self' https://*"
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" + : "default-src 'self' file:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://image.timesafari.app https://*.americancloud.com; connect-src 'self' https://api.timesafari.app https://api.endorser.ch https://test-api.endorser.ch https://fonts.googleapis.com"
"img-src 'self' data: https: blob:;" + ]
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
"style-src 'self' 'unsafe-inline';" +
"font-src 'self' data:;",
],
},
});
},
);
// Load the index.html
mainWindow
.loadFile(indexPath)
.then(() => {
logger.log("Successfully loaded index.html");
if (isDev) {
mainWindow.webContents.openDevTools();
logger.log("DevTools opened - running in dev mode");
} }
})
.catch((err) => {
logger.error("Failed to load index.html:", err);
logger.error("Attempted path:", indexPath);
}); });
// Listen for console messages from the renderer
mainWindow.webContents.on("console-message", (_event, level, message) => {
logger.log("Renderer Console:", message);
}); });
// Add right after creating the BrowserWindow // Load the index.html with modifications
mainWindow.webContents.on( try {
"did-fail-load", const appPath = getAppPath();
(event, errorCode, errorDescription) => { const wwwFolder = path.join(appPath, 'www');
logger.error("Page failed to load:", errorCode, errorDescription); const indexPath = path.join(wwwFolder, 'index.html');
},
); logDebug('Loading app from:', indexPath);
// Check if the file exists
if (fs.existsSync(indexPath)) {
// Read and modify index.html to disable service worker
let indexContent = fs.readFileSync(indexPath, 'utf8');
// 1. Add base tag for proper path resolution
indexContent = indexContent.replace('<head>',
`<head>\n <base href="file://${wwwFolder}/">`);
// 2. Disable service worker registration by replacing the script
if (indexContent.includes('registerSW.js')) {
indexContent = indexContent.replace(
/<script src="registerSW\.js"><\/script>/,
'<script>/* Service worker disabled in Electron */</script>'
);
}
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => { // Create a temp file with modified content
logger.error("Preload script error:", preloadPath, error); const tempDir = app.getPath('temp');
}); const tempIndexPath = path.join(tempDir, 'timesafari-index.html');
fs.writeFileSync(tempIndexPath, indexContent);
mainWindow.webContents.on( // Load the modified index.html
"console-message", mainWindow.loadFile(tempIndexPath).catch(err => {
(event, level, message, line, sourceId) => { logError('Failed to load via loadFile:', err);
logger.log("Renderer Console:", line, sourceId, message);
},
);
// Enable remote debugging when in dev mode // Fallback to direct URL loading
mainWindow.loadURL(`file://${tempIndexPath}`).catch(err2 => {
logError('Both loading methods failed:', err2);
mainWindow.loadURL('data:text/html,<h1>Error: Failed to load TimeSafari</h1><p>Please contact support.</p>');
});
});
} else {
logError(`Index file not found at: ${indexPath}`);
mainWindow.loadURL('data:text/html,<h1>Error: Cannot find application</h1><p>index.html not found</p>');
}
} catch (err) {
logError('Failed to load app:', err);
}
// Open DevTools in development
if (isDev) { if (isDev) {
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} }
mainWindow.on('closed', () => {
mainWindow = null;
});
} }
// Handle app ready // App lifecycle events
app.whenReady().then(createWindow); app.whenReady().then(() => {
logDebug(`Starting TimeSafari v${app.getVersion()}`);
// Handle all windows closed // Skip the service worker registration for file:// protocol
app.on("window-all-closed", () => { process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
if (process.platform !== "darwin") {
app.quit(); createWindow();
}
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
}); });
app.on("activate", () => { app.on('window-all-closed', () => {
if (BrowserWindow.getAllWindows().length === 0) { if (process.platform !== 'darwin') app.quit();
createWindow();
}
}); });
// Handle any errors // Handle uncaught exceptions
process.on("uncaughtException", (error) => { process.on('uncaughtException', (error) => {
logger.error("Uncaught Exception:", error); logError('Uncaught Exception:', error);
}); });

134
src/electron/preload.js

@ -1,78 +1,92 @@
const { contextBridge, ipcRenderer } = require("electron"); const { contextBridge, ipcRenderer } = require("electron");
const logger = { // Safety wrapper for logging
log: (message, ...args) => { function safeLog(message) {
if (process.env.NODE_ENV !== "production") { try {
/* eslint-disable no-console */ console.log('[Preload]', message);
console.log(message, ...args); } catch (e) {
/* eslint-enable no-console */ // Silent fail for logging
}
},
warn: (message, ...args) => {
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
console.warn(message, ...args);
/* eslint-enable no-console */
}
},
error: (message, ...args) => {
/* eslint-disable no-console */
console.error(message, ...args); // Errors should always be logged
/* eslint-enable no-console */
},
};
// Use a more direct path resolution approach
const getPath = (pathType) => {
switch (pathType) {
case "userData":
return (
process.env.APPDATA ||
(process.platform === "darwin"
? `${process.env.HOME}/Library/Application Support`
: `${process.env.HOME}/.local/share`)
);
case "home":
return process.env.HOME;
case "appPath":
return process.resourcesPath;
default:
return "";
} }
}; }
logger.log("Preload script starting..."); // Initialize
safeLog('Preload script starting...');
try { try {
contextBridge.exposeInMainWorld("electronAPI", { // Mock service worker registration to prevent errors
// Path utilities if (window.navigator) {
getPath, // Override the service worker registration to return a fake promise that resolves with nothing
window.navigator.serviceWorker = {
register: () => Promise.resolve({}),
getRegistration: () => Promise.resolve(null),
ready: Promise.resolve({})
};
}
// Safely expose specific APIs to the renderer process
contextBridge.exposeInMainWorld('electronAPI', {
// Basic flags/info
isElectron: true,
// Disable service worker in Electron
disableServiceWorker: true,
// Logging
log: (message) => {
try {
console.log('[Renderer]', message);
} catch (e) {
// Silence any errors from logging
}
},
// Report errors to main process
reportError: (error) => {
try {
ipcRenderer.send('app-error', error.toString());
} catch (e) {
console.error('Failed to report error to main process', e);
}
},
// Safe path handling helper (no Node modules needed)
joinPath: (...parts) => {
return parts.join('/').replace(/\/\//g, '/');
},
// Fix asset URLs
resolveAssetUrl: (assetPath) => {
if (assetPath.startsWith('/assets/')) {
return assetPath; // Already properly formed
}
if (assetPath.startsWith('assets/')) {
return '/' + assetPath; // Add leading slash
}
return assetPath;
},
// IPC functions // Send messages to main process
send: (channel, data) => { send: (channel, data) => {
const validChannels = ["toMain"]; // Whitelist channels for security
const validChannels = ['app-event', 'log-event', 'app-error'];
if (validChannels.includes(channel)) { if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data); ipcRenderer.send(channel, data);
} }
}, },
// Receive messages from main process
receive: (channel, func) => { receive: (channel, func) => {
const validChannels = ["fromMain"]; const validChannels = ['app-notification', 'log-response'];
if (validChannels.includes(channel)) { if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args)); // Remove old listeners to avoid memory leaks
ipcRenderer.removeAllListeners(channel);
// Add the new listener
ipcRenderer.on(channel, (_, ...args) => func(...args));
} }
}, }
// Environment info
env: {
isElectron: true,
isDev: process.env.NODE_ENV === "development",
},
// Path utilities
getBasePath: () => {
return process.env.NODE_ENV === "development" ? "/" : "./";
},
}); });
logger.log("Preload script completed successfully"); safeLog('Preload script completed successfully');
} catch (error) { } catch (err) {
logger.error("Error in preload script:", error); safeLog('Error in preload script: ' + err.toString());
} }

13
src/registerServiceWorker.ts

@ -37,3 +37,16 @@ if (
"Service worker registration skipped - not enabled or not in production", "Service worker registration skipped - not enabled or not in production",
); );
} }
export function registerServiceWorker() {
// Skip service worker registration in Electron
if (window.electronAPI?.isElectron) {
console.log('Running in Electron - skipping service worker registration');
return;
}
// Regular service worker registration for web
if ('serviceWorker' in navigator) {
// ... existing code ...
}
}

1
test-scripts/requirements.txt

@ -9,3 +9,4 @@ eth-utils>=2.1.0 # For Ethereum utilities
pyjwt>=2.8.0 # For JWT operations pyjwt>=2.8.0 # For JWT operations
cryptography>=42.0.0 # For key format conversion cryptography>=42.0.0 # For key format conversion
jwcrypto jwcrypto
setuptools

21
vite.config.electron.mts

@ -24,6 +24,25 @@ export default defineConfig(async () => {
}; };
} }
} }
}] }],
build: {
outDir: 'dist-electron',
emptyOutDir: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia']
}
}
},
assetsDir: 'assets',
minify: 'terser',
terserOptions: {
compress: {
drop_console: false,
},
},
},
base: './',
}); });
}); });
Loading…
Cancel
Save