feat: add error handling screen and logging functionality

This commit is contained in:
2026-06-04 14:09:27 +02:00
parent 6952a9954f
commit 982c771e82
16 changed files with 250 additions and 98 deletions
+12 -65
View File
@@ -8,8 +8,10 @@
"name": "scoreko-electron", "name": "scoreko-electron",
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"dependencies": {
"electron-log": "^5.4.4"
},
"devDependencies": { "devDependencies": {
"@electron/rebuild": "^3.7.1",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@typescript-eslint/eslint-plugin": "^8.22.0", "@typescript-eslint/eslint-plugin": "^8.22.0",
"@typescript-eslint/parser": "^8.22.0", "@typescript-eslint/parser": "^8.22.0",
@@ -182,31 +184,6 @@
"node": ">= 4.0.0" "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": { "node_modules/@electron/notarize": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
@@ -273,35 +250,6 @@
"url": "https://github.com/sponsors/gjtorikian/" "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": { "node_modules/@electron/universal": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
@@ -2971,6 +2919,15 @@
"fs-extra": "^10.1.0" "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": { "node_modules/electron-publish": {
"version": "25.1.7", "version": "25.1.7",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", "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" "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": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+8 -6
View File
@@ -99,17 +99,19 @@
"node": ">=22" "node": ">=22"
}, },
"devDependencies": { "devDependencies": {
"@electron/rebuild": "^3.7.1",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@typescript-eslint/eslint-plugin": "^8.22.0",
"@typescript-eslint/parser": "^8.22.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"electron": "39.5.1", "electron": "39.5.1",
"electron-builder": "^25.1.8", "electron-builder": "^25.1.8",
"eslint": "^9.19.0",
"prettier": "^3.4.2",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"wait-on": "^8.0.1", "wait-on": "^8.0.1"
"eslint": "^9.19.0", },
"@typescript-eslint/parser": "^8.22.0", "dependencies": {
"@typescript-eslint/eslint-plugin": "^8.22.0", "electron-log": "^5.4.4"
"prettier": "^3.4.2"
} }
} }
+24 -3
View File
@@ -5,7 +5,7 @@ import { getRemainingDelayMs } from "../utils/timing";
import { ApplicationPaths } from "./paths"; import { ApplicationPaths } from "./paths";
import { createShutdownService, ShutdownService } from "./shutdown-service"; 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 = { export type ApplicationWindow = {
close: () => void; close: () => void;
@@ -53,6 +53,7 @@ export type ApplicationController = {
focusExistingWindow: () => void; focusExistingWindow: () => void;
getState: () => ApplicationState; getState: () => ApplicationState;
launch: () => Promise<void>; launch: () => Promise<void>;
showErrorScreen: (error: unknown) => Promise<void>;
stopNodecgGracefully: () => Promise<void>; stopNodecgGracefully: () => Promise<void>;
}; };
@@ -60,7 +61,7 @@ export function createApplicationController({
appConfig, appConfig,
appVersion, appVersion,
deps, deps,
isPackaged, isPackaged: _isPackaged,
isWindows, isWindows,
paths, paths,
}: ApplicationControllerConfig): ApplicationController { }: ApplicationControllerConfig): ApplicationController {
@@ -176,7 +177,7 @@ export function createApplicationController({
} catch (error) { } catch (error) {
state = "failed"; state = "failed";
launchPromise = null; launchPromise = null;
closeLoadingWindow(); await showErrorScreen(error);
throw error; throw error;
} }
}; };
@@ -206,11 +207,31 @@ export function createApplicationController({
state = "stopped"; 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 { return {
activate, activate,
focusExistingWindow, focusExistingWindow,
getState: () => state, getState: () => state,
launch, launch,
showErrorScreen,
stopNodecgGracefully, stopNodecgGracefully,
}; };
} }
+2 -2
View File
@@ -3,6 +3,7 @@ import path from "node:path";
import { getRuntimeConfig, loadEnvFile, AppRuntimeConfig } from "../config/runtime-config"; import { getRuntimeConfig, loadEnvFile, AppRuntimeConfig } from "../config/runtime-config";
import { showFatalError, log } from "../errors/error-handler"; import { showFatalError, log } from "../errors/error-handler";
import { logger } from "../logging/logger";
import { createNodecgProcessManager } from "../nodecg/process-manager"; import { createNodecgProcessManager } from "../nodecg/process-manager";
import { prepareUserNodecgRuntime } from "../nodecg/runtime-setup"; import { prepareUserNodecgRuntime } from "../nodecg/runtime-setup";
import { scheduleUpdateCheck } from "../updates/update-service"; import { scheduleUpdateCheck } from "../updates/update-service";
@@ -97,8 +98,7 @@ export function bootstrap(): void {
} }
controller.launch().catch((error: unknown) => { controller.launch().catch((error: unknown) => {
showFatalError("No se pudo iniciar Scoreko.", error); logger.error("launch-failed", { error: error instanceof Error ? error.stack : String(error) });
app.exit(1);
}); });
}); });
+2
View File
@@ -10,6 +10,7 @@ export type ApplicationPaths = {
mainDashboardUrl: string; mainDashboardUrl: string;
loadingDashboardUrl: string; loadingDashboardUrl: string;
staticLoadingHtmlPath: string; staticLoadingHtmlPath: string;
staticErrorHtmlPath: string;
}; };
export function getRootPath(isDev: boolean, compiledMainDir: string, resourcesPath: string): string { export function getRootPath(isDev: boolean, compiledMainDir: string, resourcesPath: string): string {
@@ -80,5 +81,6 @@ export function getApplicationPaths({
mainDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.mainDashboardRoute), mainDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.mainDashboardRoute),
loadingDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.loadingDashboardRoute), loadingDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.loadingDashboardRoute),
staticLoadingHtmlPath: path.join(rootPath, "static", "loading.html"), staticLoadingHtmlPath: path.join(rootPath, "static", "loading.html"),
staticErrorHtmlPath: path.join(rootPath, "static", "error.html"),
}; };
} }
+1 -1
View File
@@ -1,4 +1,4 @@
export type AppShutdownState = "running" | "stopping" | "stopped"; type AppShutdownState = "running" | "stopping" | "stopped";
export type ShutdownService = { export type ShutdownService = {
getState: () => AppShutdownState; getState: () => AppShutdownState;
+1 -1
View File
@@ -6,7 +6,7 @@ export function log(...args: unknown[]): void {
logger.info("runtime", { args }); logger.info("runtime", { args });
} }
export function formatErrorMessage(error: unknown): string { function formatErrorMessage(error: unknown): string {
if (error instanceof Error) { if (error instanceof Error) {
const stack = error.stack?.trim(); const stack = error.stack?.trim();
return stack && stack.length > 0 ? stack : error.message; return stack && stack.length > 0 ? stack : error.message;
+8 -11
View File
@@ -1,7 +1,14 @@
import electronLog from "electron-log";
export type LogLevel = "debug" | "info" | "warn" | "error"; export type LogLevel = "debug" | "info" | "warn" | "error";
type LogContext = Record<string, unknown>; 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 { function write(level: LogLevel, message: string, context?: LogContext): void {
const payload = { const payload = {
ts: new Date().toISOString(), ts: new Date().toISOString(),
@@ -13,17 +20,7 @@ function write(level: LogLevel, message: string, context?: LogContext): void {
const line = JSON.stringify(payload); const line = JSON.stringify(payload);
if (level === "error") { electronLog[level](line);
console.error(line);
return;
}
if (level === "warn") {
console.warn(line);
return;
}
console.log(line);
} }
export const logger = { export const logger = {
+1 -1
View File
@@ -54,7 +54,7 @@ export type NodecgProcessManager = {
getState: () => NodecgProcessState; getState: () => NodecgProcessState;
}; };
export type NodecgProcessState = "idle" | "starting" | "running" | "stopping" | "stopped" | "failed"; type NodecgProcessState = "idle" | "starting" | "running" | "stopping" | "stopped" | "failed";
export function createNodecgProcessManager({ export function createNodecgProcessManager({
isDev, isDev,
+1 -1
View File
@@ -1,6 +1,6 @@
import { isRecord, readNonEmptyString } from "../utils/unknown-values"; import { isRecord, readNonEmptyString } from "../utils/unknown-values";
export type GiteaReleaseAsset = { type GiteaReleaseAsset = {
name: string; name: string;
browserDownloadUrl: string; browserDownloadUrl: string;
size?: number; size?: number;
+8 -1
View File
@@ -1,4 +1,5 @@
import { BrowserWindow, BrowserWindowConstructorOptions, shell } from "electron"; import { BrowserWindow, BrowserWindowConstructorOptions, shell } from "electron";
import electronLog from "electron-log";
import { AppRuntimeConfig } from "../config/runtime-config"; import { AppRuntimeConfig } from "../config/runtime-config";
import { DEFAULT_WINDOW_BACKGROUND, DEFAULT_WINDOW_SIZE, LOADING_WINDOW_SIZE } from "../constants"; import { DEFAULT_WINDOW_BACKGROUND, DEFAULT_WINDOW_SIZE, LOADING_WINDOW_SIZE } from "../constants";
@@ -33,6 +34,12 @@ export function createMainWindow({
}); });
window.webContents.on("will-navigate", (event, url) => { 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)) { if (shouldAllowInternalNavigation(url, mainDashboardUrl)) {
return; return;
} }
@@ -67,7 +74,7 @@ export function createLoadingWindow({
return window; return window;
} }
export function createWindowOptions({ function createWindowOptions({
allowDevTools, allowDevTools,
appConfig, appConfig,
rootPath, rootPath,
+4 -2
View File
@@ -92,6 +92,7 @@ test("ApplicationController preserves startup ordering and schedules updates aft
mainDashboardUrl: "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html?standalone=true", 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", loadingDashboardUrl: "http://localhost:9090/bundles/scoreko-dev/dashboard/loading/main.html?standalone=true",
staticLoadingHtmlPath: "/app/static/loading.html", staticLoadingHtmlPath: "/app/static/loading.html",
staticErrorHtmlPath: "/app/static/error.html",
}; };
const controller = createApplicationController({ const controller = createApplicationController({
@@ -143,7 +144,6 @@ test("ApplicationController preserves startup ordering and schedules updates aft
"create-manager", "create-manager",
"start-nodecg", "start-nodecg",
"wait-nodecg", "wait-nodecg",
`loading:load:${paths.loadingDashboardUrl}`,
`main:load:${paths.mainDashboardUrl}`, `main:load:${paths.mainDashboardUrl}`,
"main:show", "main:show",
"loading:close", "loading:close",
@@ -166,6 +166,7 @@ test("ApplicationController directly launches packaged app after runtime install
mainDashboardUrl: "http://localhost:9090/main", mainDashboardUrl: "http://localhost:9090/main",
loadingDashboardUrl: "http://localhost:9090/loading", loadingDashboardUrl: "http://localhost:9090/loading",
staticLoadingHtmlPath: "/app/static/loading.html", staticLoadingHtmlPath: "/app/static/loading.html",
staticErrorHtmlPath: "/app/static/error.html",
}, },
deps: { deps: {
createLoadingWindow: () => { createLoadingWindow: () => {
@@ -205,7 +206,6 @@ test("ApplicationController directly launches packaged app after runtime install
"create-manager", "create-manager",
"start-nodecg", "start-nodecg",
"wait-nodecg", "wait-nodecg",
"loading:load:http://localhost:9090/loading",
"main:load:http://localhost:9090/main", "main:load:http://localhost:9090/main",
"main:show", "main:show",
"loading:close", "loading:close",
@@ -228,6 +228,7 @@ test("ApplicationController activation before readiness routes through launch",
mainDashboardUrl: "http://localhost:9090/main", mainDashboardUrl: "http://localhost:9090/main",
loadingDashboardUrl: "http://localhost:9090/loading", loadingDashboardUrl: "http://localhost:9090/loading",
staticLoadingHtmlPath: "/app/static/loading.html", staticLoadingHtmlPath: "/app/static/loading.html",
staticErrorHtmlPath: "/app/static/error.html",
}, },
deps: { deps: {
createLoadingWindow: () => new MockWindow("loading", events), createLoadingWindow: () => new MockWindow("loading", events),
@@ -269,6 +270,7 @@ test("ApplicationController shutdown is idempotent", async () => {
mainDashboardUrl: "http://localhost:9090/main", mainDashboardUrl: "http://localhost:9090/main",
loadingDashboardUrl: "http://localhost:9090/loading", loadingDashboardUrl: "http://localhost:9090/loading",
staticLoadingHtmlPath: "/app/static/loading.html", staticLoadingHtmlPath: "/app/static/loading.html",
staticErrorHtmlPath: "/app/static/error.html",
}, },
deps: { deps: {
createLoadingWindow: () => new MockWindow("loading", events), createLoadingWindow: () => new MockWindow("loading", events),
+1 -1
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; 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"; const dashboardUrl = "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html";
+1 -1
View File
@@ -3,7 +3,7 @@ import { EventEmitter } from "node:events";
import { SpawnOptions } from "node:child_process"; import { SpawnOptions } from "node:child_process";
import test from "node:test"; 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", () => { test("killProcessTree validates pid before building Windows taskkill command", () => {
const spawnCalls: Array<{ command: string; args: string[]; options: SpawnOptions }> = []; const spawnCalls: Array<{ command: string; args: string[]; options: SpawnOptions }> = [];
+2 -2
View File
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import path from "node:path"; import path from "node:path";
import test from "node:test"; import test from "node:test";
import { prepareUserNodecgRuntime } from "../main/nodecg/runtime-provisioner"; import { prepareUserNodecgRuntime } from "../main/nodecg/runtime-setup";
type FakeFsState = { type FakeFsState = {
paths: Set<string>; paths: Set<string>;
@@ -45,7 +45,7 @@ function createFakeFs(initialPaths: string[] = [], initialFiles: Record<string,
return normalized.endsWith("node_modules") || normalized.endsWith("bundles"); 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.copied.push({ from: path.normalize(target), to: path.normalize(linkPath) });
state.paths.add(path.normalize(linkPath)); state.paths.add(path.normalize(linkPath));
if (target.endsWith("node_modules")) { if (target.endsWith("node_modules")) {
+174
View File
@@ -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>