mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-05 21:22:07 +00:00
feat: add error handling screen and logging functionality
This commit is contained in:
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",
|
||||
|
||||
+8
-6
@@ -99,17 +99,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) });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export type ApplicationPaths = {
|
||||
mainDashboardUrl: string;
|
||||
loadingDashboardUrl: string;
|
||||
staticLoadingHtmlPath: string;
|
||||
staticErrorHtmlPath: 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),
|
||||
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;
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -67,7 +74,7 @@ export function createLoadingWindow({
|
||||
return window;
|
||||
}
|
||||
|
||||
export function createWindowOptions({
|
||||
function createWindowOptions({
|
||||
allowDevTools,
|
||||
appConfig,
|
||||
rootPath,
|
||||
|
||||
@@ -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",
|
||||
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 +144,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",
|
||||
@@ -166,6 +166,7 @@ test("ApplicationController directly launches packaged app after runtime install
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => {
|
||||
@@ -205,7 +206,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",
|
||||
@@ -228,6 +228,7 @@ test("ApplicationController activation before readiness routes through launch",
|
||||
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),
|
||||
@@ -269,6 +270,7 @@ test("ApplicationController shutdown is idempotent", async () => {
|
||||
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),
|
||||
|
||||
@@ -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 }> = [];
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user