mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
Compare commits
7 Commits
6952a9954f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 03446a3b4b | |||
| f0a35bf655 | |||
| 934500a1db | |||
| 2496f13055 | |||
| 982c771e82 | |||
| 143ff7e8db | |||
| b0e0fdb9a1 |
@@ -11,7 +11,6 @@ SCOREKO_APP_ICON_PATH=static/icons/icon.ico
|
||||
NODECG_BUNDLE_NAME=scoreko-dev
|
||||
NODECG_PORT=9090
|
||||
SCOREKO_DASHBOARD_ROUTE=dashboard/scoreko-dev/main.html?standalone=true
|
||||
SCOREKO_LOADING_ROUTE=dashboard/loading/main.html?standalone=true
|
||||
|
||||
# Timing & Lifecycles (Required)
|
||||
ELECTRON_LOAD_DELAY_MS=10000
|
||||
|
||||
@@ -1,47 +1,41 @@
|
||||
# scoreko-electron
|
||||
# Scoreko Desktop
|
||||
|
||||
Windows desktop installer for Scoreko. The packaged app includes Electron, NodeCG, the compiled `scoreko-dev` bundle, and the production modules needed to run it, so end users do not need Node.js, pnpm, or a cloned repository.
|
||||
This is the Windows desktop wrapper for Scoreko. It bundles Electron, NodeCG, and our custom `scoreko-dev` bundle into a single standalone executable. Users just double-click the installer and everything works—no Node.js, pnpm, or command line required.
|
||||
|
||||
## Build on a development machine
|
||||
## Local Development
|
||||
|
||||
From the repository root:
|
||||
If you're working on the app locally, start by installing dependencies at the repository root:
|
||||
|
||||
```powershell
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Then from `scoreko-electron-dev`:
|
||||
Then, move into the wrapper folder:
|
||||
|
||||
```powershell
|
||||
cd scoreko-electron-dev
|
||||
npm install
|
||||
npm run dist:win
|
||||
```
|
||||
|
||||
The installer is written to `scoreko-electron-dev/release/Scoreko-setup-0.1.0.exe`.
|
||||
### Useful Commands
|
||||
|
||||
## What the build does
|
||||
- `npm run start`: Builds the bundle and launches Electron locally for testing.
|
||||
- `npm run dist:win`: Packages everything and creates the `.exe` Windows installer in the `release/` folder.
|
||||
- `npm run prepare:runtime`: Extracts a fresh NodeCG runtime from the parent bundle (useful if you changed dependencies).
|
||||
- `npm run rebuild:native`: Rebuilds native Node modules (like SQLite) specifically for Electron's V8 engine.
|
||||
- `npm run doctor`: Runs a quick sanity check to verify your local configuration and port availability.
|
||||
|
||||
- Builds the parent `scoreko-dev` bundle with `pnpm build`.
|
||||
- Creates `scoreko-electron-dev/lib/nodecg` with a small NodeCG runtime.
|
||||
- Installs production runtime modules into that runtime.
|
||||
- Rebuilds `better-sqlite3` for Electron before creating the installer.
|
||||
- Packages the runtime as an Electron extra resource outside the app archive.
|
||||
## How it works under the hood
|
||||
|
||||
## Runtime behavior
|
||||
When you build the installer, the script automatically compiles the main `scoreko-dev` bundle, provisions a lightweight NodeCG runtime in `lib/nodecg`, and packages it as an external asset alongside the Electron app.
|
||||
|
||||
On first launch, Scoreko copies the packaged NodeCG runtime to the user's app data folder and then relaunches itself before starting NodeCG. This keeps `cfg`, `db`, and `logs` writable on Windows even when the app is installed under `Program Files`, and avoids transient startup failures caused by freshly copied runtime files.
|
||||
When a user runs Scoreko for the first time, the app copies this NodeCG runtime directly into their local AppData folder. This is a deliberate choice: it ensures that databases, configs, and logs remain fully writable, even if the user installed the app in restricted directories like `Program Files`.
|
||||
|
||||
## Useful scripts
|
||||
## Auto-Updates via Gitea
|
||||
|
||||
- `npm run start`: build everything and run Electron locally.
|
||||
- `npm run prepare:runtime`: recreate `lib/nodecg` from the parent bundle.
|
||||
- `npm run rebuild:native`: rebuild NodeCG native modules for Electron.
|
||||
- `npm run dist:win`: create the Windows installer.
|
||||
- `npm run doctor`: check the prepared runtime and the configured port.
|
||||
Scoreko supports seamless, opt-in updates through your Gitea instance.
|
||||
|
||||
## Updates from Gitea
|
||||
|
||||
Scoreko can check a Gitea release feed without forcing the user to update. Edit `static/updates.json` before building:
|
||||
Before building your production installer, check `static/updates.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -52,17 +46,20 @@ Scoreko can check a Gitea release feed without forcing the user to update. Edit
|
||||
}
|
||||
```
|
||||
|
||||
For each release, bump `package.json` version, build with `npm run dist:win`, create a Gitea release tagged like `v0.2.0`, and attach `release/Scoreko-setup-0.2.0.exe`. When Scoreko sees a newer tag, it asks whether to download and install it.
|
||||
**To ship an update:**
|
||||
1. Bump the version in `package.json`.
|
||||
2. Run `npm run dist:win` to generate the new installer.
|
||||
3. Create a new release tag in Gitea (e.g., `v0.2.0`) and attach the `.exe`.
|
||||
4. The app will detect the new version, notify the user, and handle the installation safely.
|
||||
|
||||
## Configuration
|
||||
## Environment Configuration
|
||||
|
||||
The defaults match the parent bundle:
|
||||
The app ships with sensible defaults that match our development bundle:
|
||||
|
||||
- `NODECG_BUNDLE_NAME=scoreko-dev`
|
||||
- `NODECG_PORT=9090`
|
||||
- `SCOREKO_DASHBOARD_ROUTE=dashboard/scoreko-dev/main.html?standalone=true`
|
||||
- `SCOREKO_LOADING_ROUTE=dashboard/loading/main.html?standalone=true`
|
||||
- `SCOREKO_UPDATES_ENABLED=true`
|
||||
- `SCOREKO_UPDATE_ASSET_PATTERN=Scoreko-setup-.*\.exe$`
|
||||
|
||||
Copy `.env.example` only if you need local overrides while developing.
|
||||
You only need to mess with `.env.example` if you want to override these values locally while testing.
|
||||
|
||||
+21
-23
@@ -1,29 +1,27 @@
|
||||
# Main process architecture
|
||||
# Main Process Architecture
|
||||
|
||||
## Startup flow
|
||||
This document breaks down how the Electron main process is structured and what happens when the app launches.
|
||||
|
||||
1. `src/main/main.ts` loads `appConfig` from `config/runtime-config.ts`.
|
||||
2. Installs or refreshes the packaged NodeCG runtime in user data when needed (`nodecg/runtime-provisioner.ts`).
|
||||
3. Creates windows (`windows/window-factory.ts`).
|
||||
4. Starts NodeCG with `nodecg/process-manager.ts`.
|
||||
5. Waits for HTTP readiness and shows loading -> main dashboard.
|
||||
6. Checks the configured Gitea latest-release endpoint for optional updates.
|
||||
7. On shutdown, runs a single graceful-stop flow to avoid orphan processes.
|
||||
## Startup Flow
|
||||
|
||||
## Main modules
|
||||
When a user opens Scoreko, the app goes through a precise sequence to ensure NodeCG starts reliably:
|
||||
|
||||
- `config/runtime-config.ts`: read/validate env vars.
|
||||
- `nodecg/runtime-provisioner.ts`: install/refresh the managed runtime in the writable user data folder and report whether it changed.
|
||||
- `nodecg/process-manager.ts`: start, readiness, and stop for NodeCG; install/permission/port validation.
|
||||
- `updates/update-manager.ts`: optional Gitea release checks, installer download, and user-controlled install.
|
||||
- `updates/update-utils.ts`: release version comparison and installer asset selection.
|
||||
- `windows/window-factory.ts`: window creation and navigation policy.
|
||||
- `windows/navigation-security.ts`: internal navigation allowlist and safe external schemes.
|
||||
- `errors/error-presenter.ts`: fatal error presentation.
|
||||
- `errors/logger.ts`: structured logging (`info/warn/error/debug`).
|
||||
1. **Configuration:** `src/main/main.ts` kicks things off by loading `appConfig` via `config/runtime-config.ts`.
|
||||
2. **Runtime Provisioning:** The app checks the user's AppData directory. If the packaged NodeCG runtime is missing or outdated, it extracts a fresh copy (`nodecg/runtime-provisioner.ts`).
|
||||
3. **Window Creation:** The initial windows (like the loading screen) are instantiated via `windows/window-factory.ts`.
|
||||
4. **NodeCG Boot:** `nodecg/process-manager.ts` spawns the NodeCG process in the background.
|
||||
5. **Readiness Check:** The app continuously polls NodeCG until the HTTP server responds. Once ready, it transitions the UI from the loading screen to the main dashboard.
|
||||
6. **Update Check:** If updates are enabled, the app checks the configured Gitea endpoint in the background to see if a newer version is available.
|
||||
7. **Graceful Shutdown:** When the user closes the app, it triggers a unified teardown sequence to cleanly kill the NodeCG child process, preventing zombie processes from lingering in the background.
|
||||
|
||||
## Principles
|
||||
## Core Modules
|
||||
|
||||
- Mechanical refactors first.
|
||||
- Incremental hardening with conservative fallback.
|
||||
- Automated validation via `typecheck`, `build`, `test`, `doctor`, `lint`.
|
||||
Here is where the heavy lifting happens:
|
||||
|
||||
- **`config/runtime-config.ts`**: Handles environment variables and defaults.
|
||||
- **`nodecg/runtime-provisioner.ts`**: Manages copying the NodeCG runtime out of the read-only Electron package into the writable user data folder.
|
||||
- **`nodecg/process-manager.ts`**: Handles starting, polling, and killing the NodeCG server. It also validates ports and permissions before launching.
|
||||
- **`updates/update-manager.ts`**: Coordinates the Gitea update flow (checking versions, downloading installers, prompting the user).
|
||||
- **`windows/window-factory.ts`**: Centralizes window configuration and security defaults.
|
||||
- **`windows/navigation-security.ts`**: Intercepts navigation events to block unauthorized domains and safely hand off external links (like docs or emails) to the user's default browser.
|
||||
- **`errors/error-presenter.ts` & `errors/logger.ts`**: Manages structured logging (`electron-log`) and displaying the fallback error screen if boot fails.
|
||||
|
||||
+34
-36
@@ -1,45 +1,43 @@
|
||||
# Troubleshooting
|
||||
# Troubleshooting Guide
|
||||
|
||||
## `The packaged NodeCG runtime is incomplete`
|
||||
Here are some common issues you might run into while developing or using the Scoreko desktop app, along with quick fixes.
|
||||
|
||||
- Run `npm run prepare:runtime` from `scoreko-electron-dev`.
|
||||
- If the parent bundle is not installed yet, run `pnpm install` from the repository root first.
|
||||
## The app says the NodeCG runtime is incomplete
|
||||
This usually means you haven't bundled the runtime yet.
|
||||
- Run `npm run prepare:runtime` in the `scoreko-electron-dev` folder.
|
||||
- If you haven't even installed the parent bundle, go up to the repository root and run `pnpm install` first.
|
||||
|
||||
## `NodeCG is present but internal dependencies are missing`
|
||||
## NodeCG is present but internal dependencies are missing
|
||||
This happens if dependencies changed or the initial copy was interrupted.
|
||||
- Re-run `npm run prepare:runtime` to get a fresh copy.
|
||||
- If you're seeing SQLite errors when the app launches, you probably need to run `npm run rebuild:native` to compile it for Electron's V8 engine.
|
||||
|
||||
- Recreate the runtime with `npm run prepare:runtime`.
|
||||
- If native SQLite errors appear during launch, run `npm run rebuild:native` before packaging.
|
||||
## "No read/write permissions on NodeCG"
|
||||
In production, Scoreko runs NodeCG out of your AppData folder to ensure it has write access. During local development, it runs directly from the repo.
|
||||
If you see this permission error locally, another process probably has a file locked. Close any zombie Scoreko or Node processes and try `npm run start` again.
|
||||
|
||||
## `No read/write permissions on NodeCG`
|
||||
## Port 9090 is already in use
|
||||
You have another instance of NodeCG (or another web server) running on port 9090.
|
||||
- Find and kill the process using the port, or change `NODECG_PORT` in your `.env` file to something else (like 9091).
|
||||
- You can use `npm run doctor` to quickly test port availability.
|
||||
|
||||
- Installed builds run NodeCG from the user's app data folder, so this usually means the local development copy is locked.
|
||||
- Close any running Scoreko/NodeCG process and run `npm run start` again.
|
||||
## Timeout while waiting for NodeCG
|
||||
The app waits for the NodeCG HTTP server to respond. If it times out:
|
||||
- Check your terminal output. NodeCG might be crashing or hanging on startup due to a bundle error.
|
||||
- If your machine is just slow, you can increase `NODECG_STARTUP_TIMEOUT_MS` in the `.env` file.
|
||||
|
||||
## `Port <PORT> is already in use`
|
||||
## The app crashes immediately on a fresh install
|
||||
Scoreko copies the runtime to `%AppData%\scoreko\nodecg` and relaunches itself on the very first run.
|
||||
If it gets stuck in a loop or fails immediately:
|
||||
- Check if your antivirus or Windows Search Indexer is aggressively locking the files in AppData as they are being copied.
|
||||
- Try running `npm run rebuild:native` and then repackaging the app with `npm run dist:win`.
|
||||
|
||||
- Free the port or set `NODECG_PORT` in `.env`.
|
||||
- Use `npm run doctor` to validate availability before startup.
|
||||
## macOS builds are failing complaining about an icon
|
||||
The `electron-builder` config explicitly looks for a Mac icon at `static/icons/icon.icns`. If you don't have one, generate it and place it there before running the macOS build.
|
||||
|
||||
## `Timeout while waiting for NodeCG`
|
||||
|
||||
- Check the Electron/NodeCG output in the terminal.
|
||||
- Increase `NODECG_STARTUP_TIMEOUT_MS` if the environment is slow.
|
||||
- Recreate the runtime with `npm run prepare:runtime` if the bundle changed.
|
||||
|
||||
## First launch after install fails
|
||||
|
||||
- Scoreko relaunches itself automatically after a fresh runtime install.
|
||||
- If it still fails, check whether antivirus or file indexing is locking `%AppData%\scoreko\nodecg`.
|
||||
- Rebuild the installer with `npm run dist:win` after running `npm run rebuild:native`.
|
||||
|
||||
## macOS build fails because of icon
|
||||
|
||||
- The configuration expects `static/icons/icon.icns`.
|
||||
- Create that file before running macOS packaging.
|
||||
|
||||
## Updates do not appear
|
||||
|
||||
- Check that `static/updates.json` has `"enabled": true` before building the installer.
|
||||
- The `apiUrl` must point to Gitea's latest release API: `/api/v1/repos/<owner>/<repo>/releases/latest`.
|
||||
- The release tag must be newer than the installed `package.json` version, for example `v0.2.0`.
|
||||
- The release must include an installer asset matching `assetPattern`, by default `Scoreko-setup-.*\.exe$`.
|
||||
## Auto-updates aren't triggering
|
||||
If you published a new release on Gitea but the app ignores it:
|
||||
- Double check that `static/updates.json` has `"enabled": true` before you build the installer.
|
||||
- Ensure your `apiUrl` points exactly to the Gitea API: `http://gitea.../api/v1/repos/<owner>/<repo>/releases/latest`.
|
||||
- The git tag you created (e.g., `v0.2.0`) must be semantically higher than the version currently in your `package.json`.
|
||||
- Make sure the installer `.exe` you uploaded to Gitea actually matches the regex in `assetPattern` (default is `Scoreko-setup-.*\.exe$`).
|
||||
|
||||
Generated
+12
-65
@@ -8,8 +8,10 @@
|
||||
"name": "scoreko-electron",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"electron-log": "^5.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.7.1",
|
||||
"@types/node": "^22.10.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
@@ -182,31 +184,6 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/node-gyp": {
|
||||
"version": "10.2.0-electron.1",
|
||||
"resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
|
||||
"integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"env-paths": "^2.2.0",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"glob": "^8.1.0",
|
||||
"graceful-fs": "^4.2.6",
|
||||
"make-fetch-happen": "^10.2.1",
|
||||
"nopt": "^6.0.0",
|
||||
"proc-log": "^2.0.1",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.2.1",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp": "bin/node-gyp.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/notarize": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
|
||||
@@ -273,35 +250,6 @@
|
||||
"url": "https://github.com/sponsors/gjtorikian/"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz",
|
||||
"integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
|
||||
"@malept/cross-spawn-promise": "^2.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"debug": "^4.1.1",
|
||||
"detect-libc": "^2.0.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"got": "^11.7.0",
|
||||
"node-abi": "^3.45.0",
|
||||
"node-api-version": "^0.2.0",
|
||||
"ora": "^5.1.0",
|
||||
"read-binary-file-arch": "^1.0.6",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.0.5",
|
||||
"yargs": "^17.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"electron-rebuild": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
|
||||
@@ -2971,6 +2919,15 @@
|
||||
"fs-extra": "^10.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-log": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.4.tgz",
|
||||
"integrity": "sha512-istWgaXjBfURBSS8LWVW9C3jsc6+ac+tY1lXrQEOTp0lVj+a4OlO1Tmqb36GgnEUDv92DGC9VI1HNXwJinWpgA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-publish": {
|
||||
"version": "25.1.7",
|
||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz",
|
||||
@@ -5383,16 +5340,6 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/proc-log": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz",
|
||||
"integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
|
||||
+10
-7
@@ -91,7 +91,8 @@
|
||||
"installerHeaderIcon": "static/icons/icon.ico",
|
||||
"shortcutName": "Scoreko",
|
||||
"useZip": false,
|
||||
"deleteAppDataOnUninstall": true
|
||||
"deleteAppDataOnUninstall": true,
|
||||
"createDesktopShortcut": "always"
|
||||
},
|
||||
"compression": "normal"
|
||||
},
|
||||
@@ -99,17 +100,19 @@
|
||||
"node": ">=22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.7.1",
|
||||
"@types/node": "^22.10.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"electron": "39.5.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"eslint": "^9.19.0",
|
||||
"prettier": "^3.4.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.7.3",
|
||||
"wait-on": "^8.0.1",
|
||||
"eslint": "^9.19.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"prettier": "^3.4.2"
|
||||
"wait-on": "^8.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-log": "^5.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getRemainingDelayMs } from "../utils/timing";
|
||||
import { ApplicationPaths } from "./paths";
|
||||
import { createShutdownService, ShutdownService } from "./shutdown-service";
|
||||
|
||||
export type ApplicationState = "idle" | "preparing" | "starting" | "ready" | "stopping" | "stopped" | "failed";
|
||||
type ApplicationState = "idle" | "preparing" | "starting" | "ready" | "stopping" | "stopped" | "failed";
|
||||
|
||||
export type ApplicationWindow = {
|
||||
close: () => void;
|
||||
@@ -53,6 +53,7 @@ export type ApplicationController = {
|
||||
focusExistingWindow: () => void;
|
||||
getState: () => ApplicationState;
|
||||
launch: () => Promise<void>;
|
||||
showErrorScreen: (error: unknown) => Promise<void>;
|
||||
stopNodecgGracefully: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -60,7 +61,7 @@ export function createApplicationController({
|
||||
appConfig,
|
||||
appVersion,
|
||||
deps,
|
||||
isPackaged,
|
||||
isPackaged: _isPackaged,
|
||||
isWindows,
|
||||
paths,
|
||||
}: ApplicationControllerConfig): ApplicationController {
|
||||
@@ -176,7 +177,7 @@ export function createApplicationController({
|
||||
} catch (error) {
|
||||
state = "failed";
|
||||
launchPromise = null;
|
||||
closeLoadingWindow();
|
||||
await showErrorScreen(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -206,11 +207,31 @@ export function createApplicationController({
|
||||
state = "stopped";
|
||||
};
|
||||
|
||||
const showErrorScreen = async (error: unknown): Promise<void> => {
|
||||
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
const encodedMsg = encodeURIComponent(`msg=${encodeURIComponent(message)}`);
|
||||
const errorUrl = `file://${paths.staticErrorHtmlPath}#${encodedMsg}`;
|
||||
|
||||
const targetWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : loadingWindow;
|
||||
|
||||
if (!targetWindow || targetWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await targetWindow.loadURL(errorUrl);
|
||||
targetWindow.show();
|
||||
} catch {
|
||||
// If even the error screen fails to load, nothing more can be done.
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
activate,
|
||||
focusExistingWindow,
|
||||
getState: () => state,
|
||||
launch,
|
||||
showErrorScreen,
|
||||
stopNodecgGracefully,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
|
||||
import { getRuntimeConfig, loadEnvFile, AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { showFatalError, log } from "../errors/error-handler";
|
||||
import { logger } from "../logging/logger";
|
||||
import { createNodecgProcessManager } from "../nodecg/process-manager";
|
||||
import { prepareUserNodecgRuntime } from "../nodecg/runtime-setup";
|
||||
import { scheduleUpdateCheck } from "../updates/update-service";
|
||||
@@ -97,8 +98,7 @@ export function bootstrap(): void {
|
||||
}
|
||||
|
||||
controller.launch().catch((error: unknown) => {
|
||||
showFatalError("No se pudo iniciar Scoreko.", error);
|
||||
app.exit(1);
|
||||
logger.error("launch-failed", { error: error instanceof Error ? error.stack : String(error) });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ export type ApplicationPaths = {
|
||||
userDataPath: string;
|
||||
nodecgBaseUrl: string;
|
||||
mainDashboardUrl: string;
|
||||
loadingDashboardUrl: string;
|
||||
staticLoadingHtmlPath: string;
|
||||
staticErrorHtmlPath: string;
|
||||
};
|
||||
|
||||
export function getRootPath(isDev: boolean, compiledMainDir: string, resourcesPath: string): string {
|
||||
@@ -63,7 +63,7 @@ export function getApplicationPaths({
|
||||
}: {
|
||||
appConfig: Pick<
|
||||
AppRuntimeConfig,
|
||||
"bundleName" | "loadingDashboardRoute" | "mainDashboardRoute" | "nodecgPort" | "userDataDirectoryName"
|
||||
"bundleName" | "mainDashboardRoute" | "nodecgPort" | "userDataDirectoryName"
|
||||
>;
|
||||
appDataPath: string;
|
||||
compiledMainDir: string;
|
||||
@@ -78,7 +78,7 @@ export function getApplicationPaths({
|
||||
userDataPath: getUserDataPath(appDataPath, appConfig.userDataDirectoryName),
|
||||
nodecgBaseUrl: getNodecgBaseUrl(appConfig.nodecgPort),
|
||||
mainDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.mainDashboardRoute),
|
||||
loadingDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.loadingDashboardRoute),
|
||||
staticLoadingHtmlPath: path.join(rootPath, "static", "loading.html"),
|
||||
staticErrorHtmlPath: path.join(rootPath, "static", "error.html"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type AppShutdownState = "running" | "stopping" | "stopped";
|
||||
type AppShutdownState = "running" | "stopping" | "stopped";
|
||||
|
||||
export type ShutdownService = {
|
||||
getState: () => AppShutdownState;
|
||||
|
||||
@@ -9,7 +9,6 @@ export type AppRuntimeConfig = {
|
||||
nodecgPort: string;
|
||||
bundleName: string;
|
||||
mainDashboardRoute: string;
|
||||
loadingDashboardRoute: string;
|
||||
loadDelayMs: number;
|
||||
startupTimeoutMs: number;
|
||||
nodecgKillTimeoutMs: number;
|
||||
@@ -59,7 +58,6 @@ export function getRuntimeConfig(): AppRuntimeConfig {
|
||||
nodecgPort: parseRequiredEnvPort("NODECG_PORT"),
|
||||
bundleName: getRequiredEnv("NODECG_BUNDLE_NAME"),
|
||||
mainDashboardRoute: getRequiredEnv("SCOREKO_DASHBOARD_ROUTE"),
|
||||
loadingDashboardRoute: getRequiredEnv("SCOREKO_LOADING_ROUTE"),
|
||||
loadDelayMs: parseRequiredEnvIntInRange("ELECTRON_LOAD_DELAY_MS", 0, 600000),
|
||||
startupTimeoutMs: parseRequiredEnvIntInRange("NODECG_STARTUP_TIMEOUT_MS", 1000, 600000),
|
||||
nodecgKillTimeoutMs: parseRequiredEnvIntInRange("NODECG_KILL_TIMEOUT_MS", 0, 120000),
|
||||
|
||||
@@ -6,7 +6,7 @@ export function log(...args: unknown[]): void {
|
||||
logger.info("runtime", { args });
|
||||
}
|
||||
|
||||
export function formatErrorMessage(error: unknown): string {
|
||||
function formatErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
const stack = error.stack?.trim();
|
||||
return stack && stack.length > 0 ? stack : error.message;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import electronLog from "electron-log";
|
||||
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
type LogContext = Record<string, unknown>;
|
||||
|
||||
// Configure electron-log: write to file and (in dev) also to console.
|
||||
electronLog.initialize();
|
||||
electronLog.transports.file.level = "debug";
|
||||
electronLog.transports.console.level = process.env["NODE_ENV"] === "development" ? "debug" : false;
|
||||
|
||||
function write(level: LogLevel, message: string, context?: LogContext): void {
|
||||
const payload = {
|
||||
ts: new Date().toISOString(),
|
||||
@@ -13,17 +20,7 @@ function write(level: LogLevel, message: string, context?: LogContext): void {
|
||||
|
||||
const line = JSON.stringify(payload);
|
||||
|
||||
if (level === "error") {
|
||||
console.error(line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level === "warn") {
|
||||
console.warn(line);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(line);
|
||||
electronLog[level](line);
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
|
||||
@@ -54,7 +54,7 @@ export type NodecgProcessManager = {
|
||||
getState: () => NodecgProcessState;
|
||||
};
|
||||
|
||||
export type NodecgProcessState = "idle" | "starting" | "running" | "stopping" | "stopped" | "failed";
|
||||
type NodecgProcessState = "idle" | "starting" | "running" | "stopping" | "stopped" | "failed";
|
||||
|
||||
export function createNodecgProcessManager({
|
||||
isDev,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isRecord, readNonEmptyString } from "../utils/unknown-values";
|
||||
|
||||
export type GiteaReleaseAsset = {
|
||||
type GiteaReleaseAsset = {
|
||||
name: string;
|
||||
browserDownloadUrl: string;
|
||||
size?: number;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, shell } from "electron";
|
||||
import electronLog from "electron-log";
|
||||
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { DEFAULT_WINDOW_BACKGROUND, DEFAULT_WINDOW_SIZE, LOADING_WINDOW_SIZE } from "../constants";
|
||||
@@ -21,7 +22,7 @@ export function createMainWindow({
|
||||
const windowOptions = createWindowOptions({ allowDevTools, appConfig, rootPath, isLoadingWindow: false });
|
||||
const window = new BrowserWindow(windowOptions);
|
||||
|
||||
denyPermissionsByDefault(window);
|
||||
applySecurityPolicies(window, allowDevTools);
|
||||
window.setMenuBarVisibility(false);
|
||||
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
@@ -33,6 +34,12 @@ export function createMainWindow({
|
||||
});
|
||||
|
||||
window.webContents.on("will-navigate", (event, url) => {
|
||||
if (url.startsWith("app://open-logs")) {
|
||||
event.preventDefault();
|
||||
void shell.showItemInFolder(electronLog.transports.file.getFile().path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldAllowInternalNavigation(url, mainDashboardUrl)) {
|
||||
return;
|
||||
}
|
||||
@@ -58,7 +65,7 @@ export function createLoadingWindow({
|
||||
}: Omit<WindowServiceDependencies, "mainDashboardUrl">): BrowserWindow {
|
||||
const window = new BrowserWindow(createWindowOptions({ allowDevTools, appConfig, rootPath, isLoadingWindow: true }));
|
||||
|
||||
denyPermissionsByDefault(window);
|
||||
applySecurityPolicies(window, allowDevTools);
|
||||
|
||||
window.on("page-title-updated", (event) => {
|
||||
event.preventDefault();
|
||||
@@ -67,7 +74,7 @@ export function createLoadingWindow({
|
||||
return window;
|
||||
}
|
||||
|
||||
export function createWindowOptions({
|
||||
function createWindowOptions({
|
||||
allowDevTools,
|
||||
appConfig,
|
||||
rootPath,
|
||||
@@ -116,8 +123,25 @@ export function createWindowOptions({
|
||||
};
|
||||
}
|
||||
|
||||
function denyPermissionsByDefault(window: BrowserWindow): void {
|
||||
function applySecurityPolicies(window: BrowserWindow, allowDevTools: boolean): void {
|
||||
window.webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => {
|
||||
callback(false);
|
||||
});
|
||||
|
||||
window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: http://localhost:* http://127.0.0.1:*; connect-src * ws: wss:; img-src * data: blob:; media-src * data: blob:; font-src * data:;"
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!allowDevTools) {
|
||||
window.webContents.on("devtools-opened", () => {
|
||||
window.webContents.closeDevTools();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ test("getApplicationPaths keeps packaged root under Electron resources", () => {
|
||||
nodecgPort: "9090",
|
||||
bundleName: "scoreko-dev",
|
||||
mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||
loadingDashboardRoute: "dashboard/loading/main.html?standalone=true",
|
||||
},
|
||||
appDataPath: "/users/test/AppData/Roaming",
|
||||
compiledMainDir: "/app/dist/main",
|
||||
|
||||
@@ -57,7 +57,6 @@ function getBaseConfig(): AppRuntimeConfig {
|
||||
nodecgPort: "9090",
|
||||
bundleName: "scoreko-dev",
|
||||
mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||
loadingDashboardRoute: "dashboard/loading/main.html?standalone=true",
|
||||
loadDelayMs: 0,
|
||||
startupTimeoutMs: 100,
|
||||
nodecgKillTimeoutMs: 10,
|
||||
@@ -90,8 +89,8 @@ test("ApplicationController preserves startup ordering and schedules updates aft
|
||||
userDataPath: "/user-data/scoreko",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
mainDashboardUrl: "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html?standalone=true",
|
||||
loadingDashboardUrl: "http://localhost:9090/bundles/scoreko-dev/dashboard/loading/main.html?standalone=true",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
};
|
||||
|
||||
const controller = createApplicationController({
|
||||
@@ -143,7 +142,6 @@ test("ApplicationController preserves startup ordering and schedules updates aft
|
||||
"create-manager",
|
||||
"start-nodecg",
|
||||
"wait-nodecg",
|
||||
`loading:load:${paths.loadingDashboardUrl}`,
|
||||
`main:load:${paths.mainDashboardUrl}`,
|
||||
"main:show",
|
||||
"loading:close",
|
||||
@@ -164,8 +162,8 @@ test("ApplicationController directly launches packaged app after runtime install
|
||||
userDataPath: "/user-data/scoreko",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => {
|
||||
@@ -205,7 +203,6 @@ test("ApplicationController directly launches packaged app after runtime install
|
||||
"create-manager",
|
||||
"start-nodecg",
|
||||
"wait-nodecg",
|
||||
"loading:load:http://localhost:9090/loading",
|
||||
"main:load:http://localhost:9090/main",
|
||||
"main:show",
|
||||
"loading:close",
|
||||
@@ -226,8 +223,8 @@ test("ApplicationController activation before readiness routes through launch",
|
||||
userDataPath: "/user-data/scoreko",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => new MockWindow("loading", events),
|
||||
@@ -267,8 +264,8 @@ test("ApplicationController shutdown is idempotent", async () => {
|
||||
userDataPath: "/user-data/scoreko",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => new MockWindow("loading", events),
|
||||
|
||||
@@ -13,7 +13,6 @@ function getBaseConfig(): AppRuntimeConfig {
|
||||
nodecgPort: "9090",
|
||||
bundleName: "scoreko-dev",
|
||||
mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||
loadingDashboardRoute: "dashboard/loading/main.html?standalone=true",
|
||||
loadDelayMs: 10000,
|
||||
startupTimeoutMs: 30000,
|
||||
nodecgKillTimeoutMs: 2500,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "../main/windows/navigation-policy";
|
||||
import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "../main/windows/navigation";
|
||||
|
||||
const dashboardUrl = "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { EventEmitter } from "node:events";
|
||||
import { SpawnOptions } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
import { killProcessTree } from "../main/nodecg/platform-process-killer";
|
||||
import { killProcessTree } from "../main/nodecg/process-killer";
|
||||
|
||||
test("killProcessTree validates pid before building Windows taskkill command", () => {
|
||||
const spawnCalls: Array<{ command: string; args: string[]; options: SpawnOptions }> = [];
|
||||
|
||||
@@ -26,7 +26,6 @@ function getBaseConfig(): AppRuntimeConfig {
|
||||
nodecgPort: "9090",
|
||||
bundleName: "scoreko-dev",
|
||||
mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||
loadingDashboardRoute: "dashboard/loading/main.html?standalone=true",
|
||||
loadDelayMs: 10000,
|
||||
startupTimeoutMs: 100,
|
||||
nodecgKillTimeoutMs: 10,
|
||||
|
||||
@@ -214,7 +214,6 @@ test("getRuntimeConfig throws if required variables are missing", () => {
|
||||
NODECG_PORT: "9090",
|
||||
NODECG_BUNDLE_NAME: "scoreko-dev",
|
||||
SCOREKO_DASHBOARD_ROUTE: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||
SCOREKO_LOADING_ROUTE: "dashboard/loading/main.html?standalone=true",
|
||||
ELECTRON_LOAD_DELAY_MS: "10000",
|
||||
NODECG_STARTUP_TIMEOUT_MS: "120000",
|
||||
NODECG_KILL_TIMEOUT_MS: "2500",
|
||||
@@ -236,7 +235,6 @@ test("getRuntimeConfig parses successfully when all required variables are set",
|
||||
NODECG_PORT: "9191",
|
||||
NODECG_BUNDLE_NAME: "scoreko-dev-test",
|
||||
SCOREKO_DASHBOARD_ROUTE: "dashboard/scoreko-dev/test.html",
|
||||
SCOREKO_LOADING_ROUTE: "dashboard/loading/test.html",
|
||||
ELECTRON_LOAD_DELAY_MS: "5000",
|
||||
NODECG_STARTUP_TIMEOUT_MS: "60000",
|
||||
NODECG_KILL_TIMEOUT_MS: "1500",
|
||||
@@ -251,7 +249,6 @@ test("getRuntimeConfig parses successfully when all required variables are set",
|
||||
assert.equal(config.nodecgPort, "9191");
|
||||
assert.equal(config.bundleName, "scoreko-dev-test");
|
||||
assert.equal(config.mainDashboardRoute, "dashboard/scoreko-dev/test.html");
|
||||
assert.equal(config.loadingDashboardRoute, "dashboard/loading/test.html");
|
||||
assert.equal(config.loadDelayMs, 5000);
|
||||
assert.equal(config.startupTimeoutMs, 60000);
|
||||
assert.equal(config.nodecgKillTimeoutMs, 1500);
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { prepareUserNodecgRuntime } from "../main/nodecg/runtime-provisioner";
|
||||
import { prepareUserNodecgRuntime } from "../main/nodecg/runtime-setup";
|
||||
|
||||
type FakeFsState = {
|
||||
paths: Set<string>;
|
||||
@@ -45,7 +45,7 @@ function createFakeFs(initialPaths: string[] = [], initialFiles: Record<string,
|
||||
return normalized.endsWith("node_modules") || normalized.endsWith("bundles");
|
||||
},
|
||||
}),
|
||||
symlinkSync: (target: string, linkPath: string, type: string) => {
|
||||
symlinkSync: (target: string, linkPath: string, _type: string) => {
|
||||
state.copied.push({ from: path.normalize(target), to: path.normalize(linkPath) });
|
||||
state.paths.add(path.normalize(linkPath));
|
||||
if (target.endsWith("node_modules")) {
|
||||
|
||||
@@ -11,7 +11,6 @@ const baseConfig: AppRuntimeConfig = {
|
||||
nodecgPort: "9090",
|
||||
bundleName: "scoreko-dev",
|
||||
mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||
loadingDashboardRoute: "dashboard/loading/main.html?standalone=true",
|
||||
loadDelayMs: 0,
|
||||
startupTimeoutMs: 30000,
|
||||
nodecgKillTimeoutMs: 2500,
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'">
|
||||
<title>Scoreko - Error al iniciar</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #121212;
|
||||
color: #f5f5f5;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.error-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 560px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
color: #ef5350;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(245, 245, 245, 0.65);
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-family: ui-monospace, "Cascadia Code", "Consolas", monospace;
|
||||
white-space: pre-wrap;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.8125rem;
|
||||
color: rgba(245, 245, 245, 0.45);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
button:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#btn-logs {
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: #f5f5f5;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
#btn-quit {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-page">
|
||||
<div class="error-content">
|
||||
<svg class="error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
|
||||
<h1 id="error-title">Scoreko no pudo iniciar</h1>
|
||||
|
||||
<pre class="error-message" id="error-detail">Se produjo un error inesperado al iniciar el servidor interno.</pre>
|
||||
|
||||
<p class="hint">Revisa los logs para más detalles o cierra y vuelve a abrir la aplicación.</p>
|
||||
|
||||
<div class="actions">
|
||||
<button id="btn-logs">Ver logs</button>
|
||||
<button id="btn-quit">Cerrar Scoreko</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Read optional error detail injected via URL hash: error.html#msg=...
|
||||
try {
|
||||
const hash = decodeURIComponent(window.location.hash.slice(1));
|
||||
const params = new URLSearchParams(hash);
|
||||
const msg = params.get('msg');
|
||||
if (msg) {
|
||||
document.getElementById('error-detail').textContent = msg;
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore parse errors
|
||||
}
|
||||
|
||||
document.getElementById('btn-quit').addEventListener('click', () => {
|
||||
window.close();
|
||||
});
|
||||
|
||||
// btn-logs: the main process listens for this via ipc if a preload is wired,
|
||||
// otherwise we just note the action (no-op in sandbox mode).
|
||||
document.getElementById('btn-logs').addEventListener('click', () => {
|
||||
// Signal to main process via hash navigation that the user wants to open logs.
|
||||
// The main process's will-navigate handler opens external URLs, so we use a
|
||||
// custom app:// scheme that is caught and handled there.
|
||||
window.location.href = 'app://open-logs';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline';">
|
||||
<title>Scoreko</title>
|
||||
<style>
|
||||
body {
|
||||
|
||||
Reference in New Issue
Block a user