mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
Compare commits
15 Commits
3f756feca6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 03446a3b4b | |||
| f0a35bf655 | |||
| 934500a1db | |||
| 2496f13055 | |||
| 982c771e82 | |||
| 6952a9954f | |||
| 7102e3dd01 | |||
| 5da609cce4 | |||
| 0ea4c6e01b | |||
| 143ff7e8db | |||
| beb22cb438 | |||
| 88223d744c | |||
| ed5a7d0994 | |||
| d01ae1fa6b | |||
| 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.
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
# Architecture Audit
|
||||
|
||||
## Scope
|
||||
|
||||
This audit documents the current architecture of the Electron wrapper and the refactor boundaries agreed for future work. It is based on the previous architectural analysis and should be treated as the source of truth for refactor sessions.
|
||||
|
||||
The project is not being redesigned from scratch. The goal is to preserve the current working model, reduce lifecycle risk, and make Electron startup, NodeCG process management, updates, and security easier to test and maintain.
|
||||
|
||||
## Current Mental Model
|
||||
|
||||
The application is an Electron wrapper that lives almost entirely in the main process. There is no custom renderer, no preload script, and no meaningful IPC layer.
|
||||
|
||||
Electron starts a local NodeCG runtime and loads HTTP dashboards from `localhost` or `127.0.0.1`. The main process owns startup, provisioning, window creation, navigation policy, update checks, and shutdown.
|
||||
|
||||
## Existing Strengths
|
||||
|
||||
- The project already uses TypeScript strictness and has passing tests.
|
||||
- NodeCG process handling is encapsulated in a dedicated module.
|
||||
- Runtime provisioning preserves user-owned directories: `cfg`, `db`, and `logs`.
|
||||
- Navigation security is separated from window creation.
|
||||
- Packaging has a clear dependency on `lib/nodecg` as an Electron resource.
|
||||
- There is no accidental IPC surface.
|
||||
- Browser security defaults are mostly strong: `nodeIntegration: false`, `contextIsolation: true`, and `sandbox: true`.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
```text
|
||||
src/main/
|
||||
main.ts
|
||||
nodecg/
|
||||
process-manager.ts
|
||||
runtime-provisioner.ts
|
||||
windows/
|
||||
window-factory.ts
|
||||
navigation-security.ts
|
||||
updates/
|
||||
update-manager.ts
|
||||
scripts/
|
||||
build-scoreko-bundle.mjs
|
||||
```
|
||||
|
||||
## Main Process Responsibilities
|
||||
|
||||
`src/main/main.ts` currently performs several roles:
|
||||
|
||||
- Configures Electron app metadata and paths.
|
||||
- Sets `userData`.
|
||||
- Acquires the single-instance lock.
|
||||
- Prepares the managed NodeCG runtime.
|
||||
- Relaunches after first runtime installation.
|
||||
- Creates loading and main windows.
|
||||
- Starts NodeCG.
|
||||
- Waits for HTTP readiness.
|
||||
- Loads dashboards.
|
||||
- Schedules updates.
|
||||
- Handles activation and shutdown.
|
||||
|
||||
This is not unmanageable, but it is the main coupling point and should be split carefully.
|
||||
|
||||
## NodeCG Runtime
|
||||
|
||||
The current runtime strategy should be preserved:
|
||||
|
||||
- Install the managed NodeCG runtime under Electron `userData`.
|
||||
- Preserve user data directories across managed runtime replacement.
|
||||
- Run NodeCG through the Electron binary using `ELECTRON_RUN_AS_NODE`.
|
||||
- Wait for the local HTTP endpoint before loading dashboards.
|
||||
- Stop the process tree on shutdown.
|
||||
|
||||
This model is central to the product and should not be replaced during the refactor.
|
||||
|
||||
## Lifecycle Risks
|
||||
|
||||
### Import-Time Side Effects
|
||||
|
||||
`main.ts` performs work at import time, including app configuration, path setup, lock acquisition, URL calculation, and global state initialization.
|
||||
|
||||
Risk:
|
||||
|
||||
- Startup behavior is harder to test without real Electron.
|
||||
- Unit tests must work around global mutable state.
|
||||
- Future lifecycle changes are more likely to produce hidden side effects.
|
||||
|
||||
Decision:
|
||||
|
||||
- Move side-effect-free calculation into pure functions.
|
||||
- Keep Electron side effects inside the entrypoint or an explicit bootstrap layer.
|
||||
|
||||
### Mixed Orchestration
|
||||
|
||||
`main.ts` combines orchestration with concrete runtime, window, updater, delay, and shutdown behavior.
|
||||
|
||||
Risk:
|
||||
|
||||
- Adding lifecycle features increases coupling.
|
||||
- Startup and shutdown behavior is hard to cover end to end.
|
||||
- Error paths become difficult to reason about.
|
||||
|
||||
Decision:
|
||||
|
||||
- Extract a small `ApplicationController`.
|
||||
- Give it explicit lifecycle states.
|
||||
- Keep behavior equivalent before moving folders.
|
||||
|
||||
### macOS Activation
|
||||
|
||||
The `activate` flow can recreate the main window and load the dashboard without guaranteeing NodeCG is alive and ready.
|
||||
|
||||
Risk:
|
||||
|
||||
- On macOS, restoring the app after all windows are closed can race with NodeCG readiness.
|
||||
|
||||
Decision:
|
||||
|
||||
- Route activation through the same readiness-aware controller used by initial startup.
|
||||
|
||||
## Process Management Risks
|
||||
|
||||
NodeCG process management is generally well isolated, but process tree shutdown is a sensitive boundary.
|
||||
|
||||
Current concern:
|
||||
|
||||
- Windows shutdown uses `taskkill` with `shell: true`.
|
||||
- The PID is numeric, so command injection risk is low, but platform process control should be isolated and tested.
|
||||
|
||||
Decision:
|
||||
|
||||
- Move Windows and POSIX process termination into a platform adapter.
|
||||
- Keep process manager focused on NodeCG lifecycle.
|
||||
- Test process-kill command construction separately.
|
||||
|
||||
## Updater Risks
|
||||
|
||||
The updater is the clearest controlled rewrite candidate.
|
||||
|
||||
Current concerns:
|
||||
|
||||
- Remote JSON is trusted without strong schema validation.
|
||||
- Asset URLs need stricter protocol and host validation.
|
||||
- Installer integrity is not verified.
|
||||
- Download behavior should use safe temporary paths and atomic finalization.
|
||||
- User-facing Spanish text contains visible encoding corruption.
|
||||
|
||||
Decision:
|
||||
|
||||
- Rewrite the updater in small modules.
|
||||
- Validate remote update metadata before use.
|
||||
- Validate download URLs.
|
||||
- Keep dialogs and installation side effects separate from fetch and download logic.
|
||||
- Fix encoding as part of the updater cleanup.
|
||||
|
||||
## Electron Security Audit
|
||||
|
||||
Current good defaults:
|
||||
|
||||
- `nodeIntegration: false`
|
||||
- `contextIsolation: true`
|
||||
- `sandbox: true`
|
||||
- No preload script
|
||||
- No exposed IPC bridge
|
||||
|
||||
Security gaps to address:
|
||||
|
||||
- No explicit permission handler.
|
||||
- No explicit devtools policy by environment.
|
||||
- `webSecurity` should be explicitly set.
|
||||
- Navigation should remain restricted to allowed local origins.
|
||||
- CSP is constrained by NodeCG, but should be reviewed where feasible.
|
||||
|
||||
Decision:
|
||||
|
||||
- Keep the browser surface minimal.
|
||||
- Do not add preload or IPC unless a clear product need appears.
|
||||
- Harden Electron defaults explicitly even when current defaults already behave safely.
|
||||
|
||||
## Script And Packaging Risks
|
||||
|
||||
The build scripts depend on the parent repository layout.
|
||||
|
||||
Risk:
|
||||
|
||||
- CI or external workspaces may fail if the expected sibling project is missing.
|
||||
|
||||
Decision:
|
||||
|
||||
- Do not introduce a larger build framework.
|
||||
- Normalize path validation and shared constants.
|
||||
- Make script assumptions explicit and fail with clear errors.
|
||||
|
||||
## Refactor Boundary
|
||||
|
||||
Allowed controlled rewrites:
|
||||
|
||||
- Updater internals.
|
||||
- Application lifecycle controller.
|
||||
- Platform process termination adapter.
|
||||
- Path and URL calculation helpers.
|
||||
|
||||
Not allowed:
|
||||
|
||||
- Replacing the Electron plus NodeCG runtime model.
|
||||
- Adding a renderer architecture without a product requirement.
|
||||
- Adding IPC only for architectural symmetry.
|
||||
- Moving folders before behavior is protected by tests.
|
||||
- Introducing broad framework abstractions.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The project has a healthier base than a typical Electron wrapper at this stage. The refactor should make the main process boring, explicit, and testable while preserving the current NodeCG runtime model.
|
||||
|
||||
The target is not an enterprise architecture. The target is a small Electron shell with clear lifecycle ownership, hardened update and process boundaries, and minimal browser privileges.
|
||||
@@ -1,203 +0,0 @@
|
||||
# Architecture Rules
|
||||
|
||||
## Purpose
|
||||
|
||||
These rules define the constraints for future refactor sessions. They are intentionally practical: every rule should either protect behavior, reduce Electron risk, or keep the codebase easier to test.
|
||||
|
||||
## Core Rules
|
||||
|
||||
### Preserve The Product Model
|
||||
|
||||
- The app remains an Electron main-process wrapper around a local NodeCG runtime.
|
||||
- NodeCG continues to be launched locally.
|
||||
- Dashboards continue to load from approved local HTTP origins.
|
||||
- The managed runtime continues to live under Electron `userData`.
|
||||
- Runtime provisioning must preserve `cfg`, `db`, and `logs`.
|
||||
|
||||
### Avoid Unnecessary Architecture
|
||||
|
||||
- Do not add a renderer architecture unless a user-facing feature requires it.
|
||||
- Do not add a preload script unless desktop APIs must be exposed to web content.
|
||||
- Do not add IPC for organizational neatness.
|
||||
- Do not introduce broad frameworks for lifecycle, dependency injection, logging, or configuration.
|
||||
- Add an abstraction only when it removes real complexity or isolates a risky boundary.
|
||||
|
||||
### Keep Behavior Stable
|
||||
|
||||
- Preserve current behavior before reorganizing files.
|
||||
- Write tests around existing behavior before extracting lifecycle code.
|
||||
- Move files separately from behavior changes.
|
||||
- Run typecheck, tests, and lint after meaningful refactor steps.
|
||||
|
||||
## TypeScript Rules
|
||||
|
||||
- Do not use `any`.
|
||||
- Prefer explicit domain types at module boundaries.
|
||||
- Validate unknown external input before narrowing.
|
||||
- Keep pure functions pure.
|
||||
- Prefer narrow interfaces over large service objects.
|
||||
- Avoid global mutable state outside bootstrap or explicit controllers.
|
||||
|
||||
## Electron Rules
|
||||
|
||||
### BrowserWindow Defaults
|
||||
|
||||
Every application window must use secure defaults:
|
||||
|
||||
```text
|
||||
nodeIntegration: false
|
||||
contextIsolation: true
|
||||
sandbox: true
|
||||
webSecurity: true
|
||||
```
|
||||
|
||||
Additional rules:
|
||||
|
||||
- Devtools availability must be controlled by environment or explicit config.
|
||||
- Permission requests must be denied by default.
|
||||
- New-window behavior must be blocked unless explicitly allowed.
|
||||
- Navigation must be allowlisted.
|
||||
- Remote content must not gain Node.js access.
|
||||
|
||||
### Preload Rules
|
||||
|
||||
Current decision:
|
||||
|
||||
- No preload script is required.
|
||||
|
||||
If a preload becomes necessary:
|
||||
|
||||
- Keep it minimal.
|
||||
- Expose APIs only through `contextBridge`.
|
||||
- Do not expose raw `ipcRenderer`.
|
||||
- Do not include business logic in preload.
|
||||
- Validate all payloads crossing the boundary.
|
||||
- Treat preload as part of the security boundary, not as a convenience layer.
|
||||
|
||||
### Renderer Rules
|
||||
|
||||
Current decision:
|
||||
|
||||
- There is no custom renderer.
|
||||
|
||||
If a renderer is added later:
|
||||
|
||||
- It must not assume Node.js access.
|
||||
- It must communicate through typed, validated IPC only.
|
||||
- It must not own NodeCG process lifecycle.
|
||||
- It must not bypass navigation or permission policies.
|
||||
|
||||
## IPC Rules
|
||||
|
||||
Current decision:
|
||||
|
||||
- No IPC layer is needed.
|
||||
|
||||
If IPC becomes necessary:
|
||||
|
||||
- Define all channel names in one module.
|
||||
- Use explicit request and response types.
|
||||
- Validate every payload at runtime.
|
||||
- Use allowlisted handlers only.
|
||||
- Never expose filesystem, process, shell, or update primitives directly.
|
||||
- Never expose raw Electron APIs to web content.
|
||||
- Keep handlers small and delegate to tested services.
|
||||
- Return structured errors instead of throwing raw implementation details across IPC.
|
||||
|
||||
Example target shape:
|
||||
|
||||
```text
|
||||
src/main/ipc/
|
||||
channels.ts
|
||||
register-handlers.ts
|
||||
validators.ts
|
||||
src/shared/ipc/
|
||||
types.ts
|
||||
```
|
||||
|
||||
Do not create this structure until IPC is genuinely needed.
|
||||
|
||||
## NodeCG Runtime Rules
|
||||
|
||||
- Keep NodeCG process ownership in the main process.
|
||||
- Launch NodeCG with `ELECTRON_RUN_AS_NODE`.
|
||||
- Validate the runtime installation before launching it.
|
||||
- Wait for HTTP readiness before loading dashboards.
|
||||
- Treat process stdout and stderr as diagnostic information only.
|
||||
- Stop the full process tree on app shutdown.
|
||||
- Keep platform process termination behind an adapter.
|
||||
|
||||
## Filesystem Rules
|
||||
|
||||
- Filesystem behavior must live behind domain modules.
|
||||
- Runtime provisioning must never delete user-owned `cfg`, `db`, or `logs`.
|
||||
- Downloads must stay inside safe temporary directories.
|
||||
- Paths from config, remote metadata, or user-controlled sources must be validated before use.
|
||||
- Avoid scattering path construction across unrelated modules.
|
||||
|
||||
## Update Rules
|
||||
|
||||
- Treat remote update metadata as untrusted.
|
||||
- Validate update JSON with a runtime schema.
|
||||
- Validate asset URLs before download.
|
||||
- Prefer `https:` URLs for production updates.
|
||||
- Fail closed when metadata is malformed.
|
||||
- Download to a safe temporary file.
|
||||
- Finalize downloads atomically.
|
||||
- Keep fetch, validation, download, dialog, and install steps separate.
|
||||
- Fix user-facing encoding issues when touching updater text.
|
||||
- Do not execute downloaded installers unless validation has succeeded.
|
||||
|
||||
## Navigation Rules
|
||||
|
||||
- Allow only expected NodeCG dashboard origins.
|
||||
- Prefer explicit URL parsing over string prefix checks.
|
||||
- Block external navigation by default.
|
||||
- Block unexpected new-window attempts.
|
||||
- Keep navigation policy testable as pure logic where possible.
|
||||
|
||||
## Configuration Rules
|
||||
|
||||
- Parse configuration once.
|
||||
- Keep configuration access centralized.
|
||||
- Avoid reading environment variables throughout the codebase.
|
||||
- Keep runtime defaults explicit.
|
||||
- Ensure build scripts and app runtime agree on shared constants where appropriate.
|
||||
|
||||
## Process Rules
|
||||
|
||||
- Child process management must sit behind a small interface.
|
||||
- Platform-specific kill behavior must be isolated.
|
||||
- Windows process termination must validate numeric PIDs before command construction.
|
||||
- Shutdown must be idempotent.
|
||||
- Repeated quit events must not trigger duplicate process cleanup.
|
||||
|
||||
## Testing Rules
|
||||
|
||||
- Test pure path and URL functions directly.
|
||||
- Test lifecycle states without launching real Electron where possible.
|
||||
- Test updater validation with malformed metadata.
|
||||
- Test navigation allow and block cases.
|
||||
- Test process shutdown edge cases.
|
||||
- Add integration-style coverage only where unit tests cannot represent the Electron behavior.
|
||||
|
||||
## Refactor Rules
|
||||
|
||||
- Do not refactor unrelated modules in the same change.
|
||||
- Do not change formatting across the repository unless requested.
|
||||
- Do not move folders and change behavior in the same step.
|
||||
- Prefer small commits or small reviewable patches.
|
||||
- Leave existing passing tests intact unless the product behavior intentionally changes.
|
||||
|
||||
## Security Baseline
|
||||
|
||||
The secure baseline is:
|
||||
|
||||
- No Node.js in web content.
|
||||
- No preload unless needed.
|
||||
- No IPC unless needed.
|
||||
- Local navigation only.
|
||||
- Deny permissions by default.
|
||||
- Validate remote update data.
|
||||
- Validate downloaded update assets.
|
||||
- Keep process and filesystem access in main-process services only.
|
||||
@@ -1,97 +0,0 @@
|
||||
# Final Cleanup Summary
|
||||
|
||||
## Scope
|
||||
|
||||
Executed the final global cleanup pass using `docs/refactor` as the source of truth.
|
||||
|
||||
Source documents reviewed before code changes:
|
||||
|
||||
- `docs/refactor/ARCHITECTURE_AUDIT.md`
|
||||
- `docs/refactor/ARCHITECTURE_RULES.md`
|
||||
- `docs/refactor/TARGET_ARCHITECTURE.md`
|
||||
- `docs/refactor/MIGRATION_PLAN.md`
|
||||
- `docs/refactor/SESSION_HANDOFF.md`
|
||||
- `docs/refactor/PHASE_1_SUMMARY.md`
|
||||
- `docs/refactor/PHASE_1_FIX_SUMMARY.md`
|
||||
- `docs/refactor/PHASE_2_SUMMARY.md`
|
||||
- `docs/refactor/PHASE_3_SUMMARY.md`
|
||||
- `docs/refactor/PHASE_4_SUMMARY.md`
|
||||
|
||||
## Cleanup Completed
|
||||
|
||||
- Removed the unused `parseEnvInt` helper and its tests.
|
||||
- Consolidated duplicated `unknown` parsing helpers into `src/main/utils/unknown-values.ts`.
|
||||
- Narrowed `ApplicationController` path typing to reuse `ApplicationPaths`.
|
||||
- Narrowed update config module boundaries so updater settings only accept the runtime config fields they need.
|
||||
- Narrowed the NodeCG process manager child-process contract to the actual process surface it uses.
|
||||
- Removed `as unknown as ChildProcess` casts from process-manager tests.
|
||||
- Fixed update dialog Spanish text encoding.
|
||||
- Added an explicit return type to the update dialog message-box helper.
|
||||
- Renamed legacy updater test files:
|
||||
- `update-settings.test.ts` -> `update-config.test.ts`
|
||||
- `update-utils.test.ts` -> `update-schema.test.ts`
|
||||
|
||||
## Architecture Preserved
|
||||
|
||||
- No UX changes.
|
||||
- No new features.
|
||||
- No renderer, preload, or IPC layer.
|
||||
- No NodeCG runtime model changes.
|
||||
- No Electron permission expansion.
|
||||
- No broad framework or new lifecycle layer.
|
||||
- BrowserWindow security posture remains explicit.
|
||||
- Update validation and download boundaries remain separated.
|
||||
- Managed runtime preservation of `cfg`, `db`, and `logs` remains unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
Passed:
|
||||
|
||||
```text
|
||||
npm.cmd run typecheck
|
||||
npm.cmd exec -- tsc --noEmit --noUnusedLocals --noUnusedParameters
|
||||
npm.cmd test
|
||||
npm.cmd run lint
|
||||
npm.cmd run build
|
||||
npm.cmd run doctor
|
||||
```
|
||||
|
||||
Test result:
|
||||
|
||||
```text
|
||||
63 tests passing
|
||||
```
|
||||
|
||||
Sanity searches passed for production/test source:
|
||||
|
||||
```text
|
||||
rg -n "parseEnvInt\(|ActualizaciÃ|estÃ|versiÃ|cerrarÃ|update-utils|update-settings|\bany\b|unknown as|as unknown|@ts-ignore|@ts-expect-error|eslint-disable|nodeIntegration:\s*true|webSecurity:\s*false" src scripts
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- No `any` in `src` or `scripts`.
|
||||
- No `as unknown` casts remain.
|
||||
- No legacy updater module names remain in `src`.
|
||||
- No Spanish mojibake remains in update dialog source.
|
||||
- No unsafe Electron settings were introduced.
|
||||
|
||||
IPC/preload sanity:
|
||||
|
||||
```text
|
||||
rg -n "ipcMain|ipcRenderer|contextBridge|preload" src scripts
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Matches are limited to the regression test that guards the no-IPC/no-preload policy.
|
||||
|
||||
## Build Notes
|
||||
|
||||
The first non-escalated `npm.cmd run build` attempt was blocked by sandbox permissions while creating generated parent-repo output at:
|
||||
|
||||
```text
|
||||
C:\Users\pcantos\Documents\scoreko-dev\shared\dist
|
||||
```
|
||||
|
||||
The escalated rerun passed. The build emitted existing dependency/deprecation warnings during runtime dependency installation, but completed successfully and `npm.cmd run doctor` validated the prepared runtime.
|
||||
@@ -1,310 +0,0 @@
|
||||
# Migration Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Refactor the Electron main-process architecture without changing product behavior. The plan is sequential and should be followed in order so future sessions can make progress without reinterpreting the architecture.
|
||||
|
||||
## Migration Principles
|
||||
|
||||
- Preserve behavior before moving structure.
|
||||
- Add tests around risky lifecycle behavior before refactoring it.
|
||||
- Keep the NodeCG runtime model intact.
|
||||
- Keep Electron minimal.
|
||||
- Avoid introducing IPC, preload, or renderer code unless required by a concrete feature.
|
||||
- Prefer small pure functions for paths, URLs, update metadata, asset selection, and navigation policy.
|
||||
- Rewrite only the degraded areas: updater, lifecycle orchestration, and platform process shutdown.
|
||||
|
||||
## Phase 1: Freeze Existing Behavior
|
||||
|
||||
Purpose:
|
||||
|
||||
Protect current startup, shutdown, provisioning, navigation, and update behavior before extracting modules.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add tests for first-run runtime provisioning and relaunch behavior.
|
||||
- Add tests for loading window and main window ordering.
|
||||
- Add tests for update scheduling behavior.
|
||||
- Add tests for shutdown when NodeCG is running.
|
||||
- Add tests for shutdown when NodeCG is already stopped.
|
||||
- Add tests for double quit or repeated shutdown calls.
|
||||
- Add tests for macOS-style activation after windows are closed.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Existing tests continue to pass.
|
||||
- New tests describe current behavior, not the desired future behavior.
|
||||
- No folder reorganization occurs in this phase.
|
||||
|
||||
## Phase 2: Extract Pure Path And URL Logic
|
||||
|
||||
Purpose:
|
||||
|
||||
Remove deterministic calculations from `main.ts` while keeping Electron side effects in the entrypoint.
|
||||
|
||||
Target files:
|
||||
|
||||
```text
|
||||
src/main/app/paths.ts
|
||||
src/main/config/runtime-config.ts
|
||||
```
|
||||
|
||||
Tasks:
|
||||
|
||||
- Extract `userData` path calculation.
|
||||
- Extract managed runtime path calculation.
|
||||
- Extract NodeCG dashboard URL construction.
|
||||
- Extract runtime configuration parsing.
|
||||
- Keep Electron `app.setPath`, `app.setName`, and lock handling in bootstrap code.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Extracted functions are pure.
|
||||
- Tests cover path and URL edge cases.
|
||||
- No Electron app object is required to test the extracted logic.
|
||||
|
||||
## Phase 3: Introduce ApplicationController
|
||||
|
||||
Purpose:
|
||||
|
||||
Make lifecycle explicit without redesigning the app.
|
||||
|
||||
Target file:
|
||||
|
||||
```text
|
||||
src/main/app/application-controller.ts
|
||||
```
|
||||
|
||||
Required states:
|
||||
|
||||
```text
|
||||
idle
|
||||
preparing
|
||||
starting
|
||||
ready
|
||||
stopping
|
||||
stopped
|
||||
failed
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Prepare runtime.
|
||||
- Start NodeCG.
|
||||
- Wait for readiness.
|
||||
- Create or reuse windows through a window service.
|
||||
- Load dashboards only after NodeCG readiness.
|
||||
- Schedule update checks.
|
||||
- Handle activation safely.
|
||||
- Handle shutdown idempotently.
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- Direct process-kill command construction.
|
||||
- Direct update metadata parsing.
|
||||
- Direct Electron `BrowserWindow` option construction.
|
||||
- Business logic inside preload or renderer code.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- `main.ts` becomes a thin bootstrap.
|
||||
- Activation uses readiness-aware startup behavior.
|
||||
- Shutdown is idempotent.
|
||||
- Tests cover state transitions and failure paths.
|
||||
|
||||
## Phase 4: Extract Shutdown Service
|
||||
|
||||
Purpose:
|
||||
|
||||
Make shutdown behavior predictable and easy to test.
|
||||
|
||||
Target files:
|
||||
|
||||
```text
|
||||
src/main/app/shutdown-service.ts
|
||||
src/main/nodecg/nodecg-process-service.ts
|
||||
```
|
||||
|
||||
Tasks:
|
||||
|
||||
- Centralize app shutdown sequencing.
|
||||
- Ensure NodeCG is stopped before app exit completes.
|
||||
- Handle repeated shutdown requests.
|
||||
- Handle process already exited.
|
||||
- Preserve current quit behavior.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Repeated quit calls do not duplicate process termination.
|
||||
- Tests cover `before-quit`, process exit before shutdown, and shutdown failure logging.
|
||||
|
||||
## Phase 5: Rewrite Updater In Small Modules
|
||||
|
||||
Purpose:
|
||||
|
||||
Replace the fragile updater internals while preserving user-visible behavior.
|
||||
|
||||
Target files:
|
||||
|
||||
```text
|
||||
src/main/updates/update-service.ts
|
||||
src/main/updates/update-config.ts
|
||||
src/main/updates/update-download.ts
|
||||
src/main/updates/update-schema.ts
|
||||
```
|
||||
|
||||
Tasks:
|
||||
|
||||
- Define a runtime schema for update metadata.
|
||||
- Validate remote JSON before using it.
|
||||
- Validate update asset URLs.
|
||||
- Restrict protocols to `https:` unless an explicit local development mode exists.
|
||||
- Select platform assets through pure functions.
|
||||
- Download to a safe temporary path.
|
||||
- Finalize downloads atomically.
|
||||
- Keep dialog behavior outside fetch and download helpers.
|
||||
- Correct Spanish encoding issues.
|
||||
- Add tests for malformed JSON, missing assets, invalid URLs, failed downloads, and cancelled dialogs.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Invalid update metadata fails closed.
|
||||
- Downloaded files cannot escape the intended temporary directory.
|
||||
- User-facing strings render correctly.
|
||||
- Existing update behavior is preserved where valid metadata is provided.
|
||||
|
||||
## Phase 6: Isolate Platform Process Termination
|
||||
|
||||
Purpose:
|
||||
|
||||
Keep OS-specific process-kill details outside NodeCG lifecycle logic.
|
||||
|
||||
Target file:
|
||||
|
||||
```text
|
||||
src/main/nodecg/platform-process-killer.ts
|
||||
```
|
||||
|
||||
Tasks:
|
||||
|
||||
- Implement a small interface for terminating process trees.
|
||||
- Provide Windows and POSIX implementations.
|
||||
- Validate that Windows PIDs are numeric before command construction.
|
||||
- Avoid spreading platform conditionals through the process manager.
|
||||
- Test command selection and error handling.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- `taskkill` construction is isolated.
|
||||
- POSIX process termination is isolated.
|
||||
- Process manager tests no longer need to know platform command details.
|
||||
|
||||
## Phase 7: Harden Electron Window Policy
|
||||
|
||||
Purpose:
|
||||
|
||||
Make Electron browser security explicit and testable.
|
||||
|
||||
Target files:
|
||||
|
||||
```text
|
||||
src/main/windows/window-service.ts
|
||||
src/main/windows/navigation-policy.ts
|
||||
```
|
||||
|
||||
Tasks:
|
||||
|
||||
- Explicitly set `webSecurity: true`.
|
||||
- Keep `nodeIntegration: false`.
|
||||
- Keep `contextIsolation: true`.
|
||||
- Keep `sandbox: true`.
|
||||
- Add a permission request handler that denies by default.
|
||||
- Define devtools policy by environment.
|
||||
- Keep navigation allowlist limited to approved local NodeCG origins.
|
||||
- Prevent unexpected new-window behavior.
|
||||
- Review CSP options for NodeCG-hosted content where feasible.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- BrowserWindow options are covered by tests.
|
||||
- Permission requests are denied unless explicitly allowed.
|
||||
- Navigation policy has tests for allowed and blocked origins.
|
||||
|
||||
## Phase 8: Normalize Scripts And Shared Constants
|
||||
|
||||
Purpose:
|
||||
|
||||
Reduce packaging fragility without changing the build system.
|
||||
|
||||
Targets:
|
||||
|
||||
```text
|
||||
scripts/
|
||||
src/main/config/
|
||||
```
|
||||
|
||||
Tasks:
|
||||
|
||||
- Make repository layout assumptions explicit.
|
||||
- Validate required paths before build work starts.
|
||||
- Share package/runtime constants where reasonable.
|
||||
- Improve error messages for missing parent project or missing NodeCG runtime.
|
||||
- Keep scripts simple `.mjs` utilities.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- CI failures explain missing external dependencies clearly.
|
||||
- Local build behavior remains unchanged.
|
||||
- No new framework is introduced.
|
||||
|
||||
## Phase 9: Reorganize Folders Last
|
||||
|
||||
Purpose:
|
||||
|
||||
Move files only after behavior is protected and new ownership is clear.
|
||||
|
||||
Target structure:
|
||||
|
||||
```text
|
||||
src/main/
|
||||
app/
|
||||
config/
|
||||
windows/
|
||||
nodecg/
|
||||
updates/
|
||||
logging/
|
||||
src/shared/
|
||||
```
|
||||
|
||||
Tasks:
|
||||
|
||||
- Move files into target folders after tests pass.
|
||||
- Update imports mechanically.
|
||||
- Avoid changing logic during moves.
|
||||
- Run typecheck, tests, and lint after each group of moves.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- File moves are behavior-neutral.
|
||||
- Test output remains unchanged.
|
||||
- Imports reflect the target architecture.
|
||||
|
||||
## Required Verification Per Phase
|
||||
|
||||
Run the relevant subset while iterating, then run all checks before closing a phase:
|
||||
|
||||
```text
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Stop and reassess if:
|
||||
|
||||
- A change requires adding IPC without a product need.
|
||||
- A refactor changes the NodeCG runtime model.
|
||||
- Update validation requires a product or release-server decision.
|
||||
- CSP changes break NodeCG dashboards.
|
||||
- CI requires external repository layout decisions outside this package.
|
||||
@@ -1,92 +0,0 @@
|
||||
# Phase 1 Fix Summary
|
||||
|
||||
## Scope
|
||||
|
||||
This fix restores the Electron renderer after the Phase 1 architecture extraction. It keeps the new main-process structure, does not add a custom renderer, preload, or IPC layer, and does not revert the Phase 1 refactor.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Phase 1 moved the real bootstrap module from `src/main/main.ts` to `src/main/app/bootstrap.ts`. After compilation, that changed the runtime `__dirname` from:
|
||||
|
||||
```text
|
||||
dist/main
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```text
|
||||
dist/main/app
|
||||
```
|
||||
|
||||
The development root calculation still treated `__dirname` as if it were `dist/main`, so `rootPath` resolved to `dist` instead of the repository root. Electron then looked for the packaged NodeCG runtime at:
|
||||
|
||||
```text
|
||||
dist/lib/nodecg
|
||||
```
|
||||
|
||||
instead of:
|
||||
|
||||
```text
|
||||
lib/nodecg
|
||||
```
|
||||
|
||||
That prevented the main process from preparing and launching the correct NodeCG runtime, leaving the BrowserWindow without a valid dashboard to render.
|
||||
|
||||
During verification, a second compatibility issue appeared in the packaged runtime: the Vite/NodeCG bundle imports generated files from the bundle-level `nodecg` directory, but `prepare-nodecg-runtime.mjs` did not copy that directory into `lib/nodecg/bundles/scoreko-dev`. Without it, NodeCG could start but failed to mount the `scoreko-dev` extension, and dashboard URLs returned 404.
|
||||
|
||||
## Changes Made
|
||||
|
||||
- `src/main/app/bootstrap.ts`
|
||||
- Passes `dist/main` into the path helper by resolving one level above the compiled bootstrap directory.
|
||||
- Keeps path ownership in `src/main/app/paths.ts` and preserves the extracted bootstrap architecture.
|
||||
|
||||
- `scripts/prepare-nodecg-runtime.mjs`
|
||||
- Copies the generated bundle `nodecg` directory into the managed NodeCG runtime.
|
||||
- Treats that directory as required runtime output so an incomplete Vite/NodeCG build fails early.
|
||||
|
||||
- `src/main/nodecg/runtime-provisioner.ts`
|
||||
- Refreshes the managed runtime when the source runtime manifest was regenerated, even if the app and bundle versions are unchanged.
|
||||
- Still preserves user-owned `cfg`, `db`, and `logs`.
|
||||
|
||||
- `src/tests/runtime-provisioner.test.ts`
|
||||
- Adds coverage for refreshing managed runtime files when the source runtime manifest changes.
|
||||
|
||||
## Verification
|
||||
|
||||
Commands run successfully:
|
||||
|
||||
```text
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Current test result:
|
||||
|
||||
```text
|
||||
53 tests passing
|
||||
```
|
||||
|
||||
Runtime verification:
|
||||
|
||||
- Rebuilt the NodeCG runtime.
|
||||
- Rebuilt `better-sqlite3` for Electron 39.5.1.
|
||||
- Started Electron with updates disabled and load delay set to zero.
|
||||
- Confirmed NodeCG served:
|
||||
- `http://127.0.0.1:9090` with `200 OK`
|
||||
- `http://localhost:9090/bundles/scoreko-dev/dashboard/scoreko-dev/main.html?standalone=true` with `200 OK`
|
||||
- `http://localhost:9090/bundles/scoreko-dev/dashboard/loading/main.html?standalone=true` with `200 OK`
|
||||
- Confirmed the Electron renderer target loaded:
|
||||
- title: `Dashboard`
|
||||
- URL: `http://localhost:9090/bundles/scoreko-dev/dashboard/scoreko-dev/main.html?standalone=true#/`
|
||||
- DOM element count: `311`
|
||||
- visible body text included the Scoreko dashboard navigation and controls.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- No preload was added.
|
||||
- No IPC was added.
|
||||
- No custom renderer architecture was added.
|
||||
- BrowserWindow security settings remain explicit.
|
||||
- NodeCG remains owned by the main process.
|
||||
- Dashboard loading remains gated behind NodeCG readiness.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Phase 1 Summary
|
||||
|
||||
## Scope
|
||||
|
||||
Executed the architecture base refactor only. The change keeps the Electron plus local NodeCG product model intact and does not add a renderer, preload, or IPC layer.
|
||||
|
||||
Documentation used as source of truth:
|
||||
|
||||
- `docs/refactor/ARCHITECTURE_AUDIT.md`
|
||||
- `docs/refactor/ARCHITECTURE_RULES.md`
|
||||
- `docs/refactor/TARGET_ARCHITECTURE.md`
|
||||
- `docs/refactor/MIGRATION_PLAN.md`
|
||||
- `docs/refactor/SESSION_HANDOFF.md`
|
||||
|
||||
## Completed
|
||||
|
||||
- Split the Electron entrypoint into a thin `src/main/main.ts` and explicit bootstrap logic in `src/main/app/bootstrap.ts`.
|
||||
- Added `src/main/app/application-controller.ts` to own startup, activation, update scheduling, and shutdown coordination.
|
||||
- Added `src/main/app/paths.ts` for pure root path, userData path, NodeCG runtime path, and dashboard URL construction.
|
||||
- Added `src/main/app/shutdown-service.ts` so repeated shutdown requests reuse one stop operation.
|
||||
- Renamed window ownership toward the target architecture:
|
||||
- `src/main/windows/window-service.ts`
|
||||
- `src/main/windows/navigation-policy.ts`
|
||||
- Moved logging to `src/main/logging/logger.ts`.
|
||||
- Extracted process-tree termination to `src/main/nodecg/platform-process-killer.ts`.
|
||||
- Normalized imports away from old `window-factory`, `navigation-security`, and `errors/logger` paths.
|
||||
- Made BrowserWindow security settings explicit:
|
||||
- `nodeIntegration: false`
|
||||
- `contextIsolation: true`
|
||||
- `sandbox: true`
|
||||
- `webSecurity: true`
|
||||
- devtools controlled by development mode
|
||||
- permissions denied by default
|
||||
- Added architecture-base tests for:
|
||||
- path and URL helpers
|
||||
- application startup ordering
|
||||
- packaged relaunch after runtime installation
|
||||
- activation before readiness
|
||||
- shutdown idempotency
|
||||
- platform process killing
|
||||
|
||||
## Intentionally Not Changed
|
||||
|
||||
- No UX changes.
|
||||
- No custom renderer.
|
||||
- No preload script.
|
||||
- No IPC layer.
|
||||
- No packaging changes.
|
||||
- No update-manager rewrite.
|
||||
- No complex NodeCG lifecycle rewrite.
|
||||
- No change to the managed runtime location under Electron `userData`.
|
||||
- No change to preservation of `cfg`, `db`, and `logs`.
|
||||
- No change to launching NodeCG with `ELECTRON_RUN_AS_NODE`.
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
- Startup still prepares the managed NodeCG runtime before launching NodeCG.
|
||||
- Packaged first-run runtime installation still relaunches before NodeCG starts.
|
||||
- Loading and main windows are still created before NodeCG readiness so the startup experience remains equivalent.
|
||||
- Dashboard loading remains gated behind NodeCG readiness.
|
||||
- Update checks are still scheduled only after the main window is shown.
|
||||
- Shutdown remains idempotent and still stops the NodeCG process tree.
|
||||
- macOS-style activation now routes through the controller so dashboard loading cannot bypass readiness.
|
||||
|
||||
## Verification
|
||||
|
||||
Commands run successfully:
|
||||
|
||||
```text
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Current test result:
|
||||
|
||||
```text
|
||||
52 tests passing
|
||||
```
|
||||
|
||||
Import/security sanity search:
|
||||
|
||||
```text
|
||||
rg -n "navigation-security|window-factory|errors/logger|preload|ipcRenderer|ipcMain|nodeIntegration:\s*true|webSecurity:\s*false|any\b" src docs/refactor
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- No legacy imports or unsafe Electron settings remain in `src`.
|
||||
- Remaining matches are source-of-truth documentation references only.
|
||||
@@ -1,79 +0,0 @@
|
||||
# Phase 2 Summary
|
||||
|
||||
## Scope
|
||||
|
||||
Executed the IPC and process-management phase only.
|
||||
|
||||
Documentation used as source of truth:
|
||||
|
||||
- `docs/refactor/ARCHITECTURE_AUDIT.md`
|
||||
- `docs/refactor/ARCHITECTURE_RULES.md`
|
||||
- `docs/refactor/TARGET_ARCHITECTURE.md`
|
||||
- `docs/refactor/MIGRATION_PLAN.md`
|
||||
- `docs/refactor/SESSION_HANDOFF.md`
|
||||
|
||||
## IPC And Preload Decision
|
||||
|
||||
No IPC or preload layer was added.
|
||||
|
||||
This is intentional. The current architecture defines a zero-surface IPC model as the secure target because there is no custom renderer and no product requirement for desktop APIs to cross into web content.
|
||||
|
||||
To make that decision enforceable, a regression test now scans `src/main` and fails if main-process source introduces:
|
||||
|
||||
- `ipcMain`
|
||||
- `ipcRenderer`
|
||||
- `contextBridge`
|
||||
- `preload`
|
||||
|
||||
## Process Management Changes
|
||||
|
||||
- Narrowed `NodecgProcessManager` so `startNodecgProcess` no longer returns the raw `ChildProcess`.
|
||||
- Removed the public internal `getProcess` escape hatch from `NodecgProcessManager`.
|
||||
- Added explicit NodeCG process states:
|
||||
- `idle`
|
||||
- `starting`
|
||||
- `running`
|
||||
- `stopping`
|
||||
- `stopped`
|
||||
- `failed`
|
||||
- Added `getState` as the narrow observable process-management API.
|
||||
- Made NodeCG startup idempotent while an async startup is already in progress.
|
||||
- Prevented new startup while process shutdown is in progress.
|
||||
- Preserved process-tree termination through `platform-process-killer.ts`.
|
||||
- Preserved `ELECTRON_RUN_AS_NODE`, `shell: false`, `windowsHide: true`, and detached POSIX process-group behavior.
|
||||
|
||||
## Security Notes
|
||||
|
||||
- No raw Electron IPC APIs are imported in production source.
|
||||
- No preload script is configured or exposed.
|
||||
- No renderer/main business logic boundary was added.
|
||||
- No filesystem, process, shell, or update primitives were exposed to web content.
|
||||
- BrowserWindow security settings from Phase 1 remain unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
Commands run successfully:
|
||||
|
||||
```text
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Current test result:
|
||||
|
||||
```text
|
||||
55 tests passing
|
||||
```
|
||||
|
||||
Additional sanity search:
|
||||
|
||||
```text
|
||||
rg -n "ipcMain|ipcRenderer|contextBridge|preload|nodeIntegration:\s*true|webSecurity:\s*false|\bany\b" src/main src/tests
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- No production IPC or preload surface exists.
|
||||
- No unsafe Electron settings were introduced.
|
||||
- Remaining IPC/preload string matches are limited to the regression test that guards the zero-surface policy.
|
||||
@@ -1,107 +0,0 @@
|
||||
# Phase 3 Summary
|
||||
|
||||
## Scope
|
||||
|
||||
Executed the UI and settings cleanup phase only for the Electron package.
|
||||
|
||||
Documentation used as source of truth:
|
||||
|
||||
- `docs/refactor/ARCHITECTURE_AUDIT.md`
|
||||
- `docs/refactor/ARCHITECTURE_RULES.md`
|
||||
- `docs/refactor/TARGET_ARCHITECTURE.md`
|
||||
- `docs/refactor/MIGRATION_PLAN.md`
|
||||
- `docs/refactor/SESSION_HANDOFF.md`
|
||||
|
||||
## Changes Made
|
||||
|
||||
- Split update dialog UI out of `src/main/updates/update-manager.ts` into `src/main/updates/update-dialogs.ts`.
|
||||
- Split update settings loading and file-config normalization into `src/main/updates/update-settings.ts`.
|
||||
- Split installer download behavior into `src/main/updates/update-download.ts`.
|
||||
- Kept `src/main/updates/update-manager.ts` focused on orchestration:
|
||||
- load settings
|
||||
- fetch latest release
|
||||
- ask the user what to do
|
||||
- download installer
|
||||
- run install handoff
|
||||
- Added defensive update config parsing from `unknown` JSON without introducing `any`.
|
||||
- Added settings tests covering:
|
||||
- runtime config disabling updates
|
||||
- runtime config overriding file settings
|
||||
- malformed update config normalization
|
||||
- invalid JSON fallback and logging
|
||||
- Fixed the existing Spanish mojibake in update dialogs touched by this phase.
|
||||
|
||||
## Intentionally Not Changed
|
||||
|
||||
- No UX flow changes.
|
||||
- No new features.
|
||||
- No custom renderer was added.
|
||||
- No preload was added.
|
||||
- No IPC was added.
|
||||
- No parent bundle source was modified.
|
||||
- No generated `dist` or `lib` source was edited manually.
|
||||
- No forms or controls were changed in the NodeCG dashboard.
|
||||
|
||||
## Verification
|
||||
|
||||
Commands run successfully:
|
||||
|
||||
```text
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Current test result:
|
||||
|
||||
```text
|
||||
59 tests passing
|
||||
```
|
||||
|
||||
Sanity searches:
|
||||
|
||||
```text
|
||||
rg -n "\bany\b" src/main src/tests
|
||||
rg -n "ActualizaciÃ|estÃ|versiÃ|cerrarÃ" src/main src/tests
|
||||
rg -n "ipcMain|ipcRenderer|contextBridge|preload|nodeIntegration:\s*true|webSecurity:\s*false" src/main src/tests
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- No `any` was introduced.
|
||||
- No touched Spanish update-dialog text remains mojibaked.
|
||||
- No production IPC or preload surface exists.
|
||||
- No unsafe Electron window settings were introduced.
|
||||
- Remaining IPC/preload matches are limited to the regression test that guards the zero-surface policy.
|
||||
|
||||
## UI Verification
|
||||
|
||||
The Electron launch path prepared a temporary managed runtime, but the NodeCG child did not expose port `9090` within the verification window. To verify the served UI without touching the user's real runtime data, NodeCG was launched from a temporary Electron `userData` directory:
|
||||
|
||||
```text
|
||||
SCOREKO_APP_USER_DATA_DIRECTORY=scoreko-codex-ui-check
|
||||
SCOREKO_UPDATES_ENABLED=false
|
||||
ELECTRON_LOAD_DELAY_MS=0
|
||||
```
|
||||
|
||||
The temporary NodeCG runtime served:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:9090 -> 200 OK
|
||||
```
|
||||
|
||||
Browser verification loaded:
|
||||
|
||||
```text
|
||||
http://localhost:9090/bundles/scoreko-dev/dashboard/scoreko-dev/main.html?standalone=true#/
|
||||
```
|
||||
|
||||
Observed UI signals:
|
||||
|
||||
- Page title: `Dashboard`
|
||||
- Scoreko sidebar rendered.
|
||||
- Main navigation rendered.
|
||||
- `Settings` navigation entry rendered.
|
||||
- Dashboard form controls rendered.
|
||||
|
||||
The temporary NodeCG process was stopped after verification.
|
||||
@@ -1,121 +0,0 @@
|
||||
# Phase 4 Summary
|
||||
|
||||
## Scope
|
||||
|
||||
Executed only the filesystem, updater, and packaging/build-config cleanup requested for this phase.
|
||||
|
||||
Documentation used as source of truth:
|
||||
|
||||
- `docs/refactor/ARCHITECTURE_AUDIT.md`
|
||||
- `docs/refactor/ARCHITECTURE_RULES.md`
|
||||
- `docs/refactor/TARGET_ARCHITECTURE.md`
|
||||
- `docs/refactor/MIGRATION_PLAN.md`
|
||||
- `docs/refactor/SESSION_HANDOFF.md`
|
||||
|
||||
## Filesystem And Paths
|
||||
|
||||
- Added pure path helpers in `src/main/app/paths.ts` for:
|
||||
- managed NodeCG runtime storage under Electron `userData`
|
||||
- default update config location
|
||||
- update download temp directory
|
||||
- safe child-path resolution that rejects traversal and absolute-path escape
|
||||
- Updated runtime provisioning to use the managed-runtime path helper instead of rebuilding that storage path locally.
|
||||
- Added tests for update storage paths and path traversal rejection.
|
||||
|
||||
## Updater
|
||||
|
||||
- Reorganized updater modules toward the target architecture:
|
||||
- `src/main/updates/update-service.ts`
|
||||
- `src/main/updates/update-config.ts`
|
||||
- `src/main/updates/update-schema.ts`
|
||||
- `src/main/updates/update-download.ts`
|
||||
- Removed the older updater module names:
|
||||
- `update-manager.ts`
|
||||
- `update-settings.ts`
|
||||
- `update-utils.ts`
|
||||
- Added runtime validation for remote Gitea release metadata before building update state.
|
||||
- Added URL policy handling so packaged builds reject insecure HTTP update URLs and installer downloads.
|
||||
- Kept local development able to use HTTP update endpoints explicitly through the dev policy.
|
||||
- Changed installer download behavior to:
|
||||
- validate URL protocol before fetch
|
||||
- sanitize installer file names
|
||||
- constrain output to the safe temp download directory
|
||||
- write to a staging file first
|
||||
- finalize with atomic rename
|
||||
- clean staging files on failure
|
||||
- Kept dialogs and install handoff separate from schema parsing and download streaming.
|
||||
|
||||
## Packaging And Build Config
|
||||
|
||||
- Added `scripts/build-config.mjs` as the shared build-layout source for scripts.
|
||||
- Consolidated repeated script constants for:
|
||||
- Electron package root
|
||||
- parent Scoreko bundle root
|
||||
- packaged NodeCG runtime root
|
||||
- bundle name
|
||||
- generated bundle entries
|
||||
- prepared runtime entries
|
||||
- npm/electron cache locations
|
||||
- local binary path resolution
|
||||
- Updated packaging-related scripts to use the shared config:
|
||||
- `scripts/build-scoreko-bundle.mjs`
|
||||
- `scripts/prepare-nodecg-runtime.mjs`
|
||||
- `scripts/rebuild-nodecg-native.mjs`
|
||||
- `scripts/doctor.mjs`
|
||||
- Improved the missing parent-project error in `build-scoreko-bundle.mjs` so CI/local failures report the expected layout and missing markers.
|
||||
|
||||
## Intentionally Not Changed
|
||||
|
||||
- No UX changes.
|
||||
- No custom renderer.
|
||||
- No preload.
|
||||
- No IPC.
|
||||
- No Electron window behavior changes.
|
||||
- No NodeCG runtime model changes.
|
||||
- No user-owned runtime directory deletion changes.
|
||||
- No broad build framework introduced.
|
||||
- No `any` added.
|
||||
|
||||
## Verification
|
||||
|
||||
Commands run successfully:
|
||||
|
||||
```text
|
||||
npm.cmd run typecheck
|
||||
npm.cmd test
|
||||
npm.cmd run lint
|
||||
npm.cmd run doctor
|
||||
```
|
||||
|
||||
Current test result:
|
||||
|
||||
```text
|
||||
65 tests passing
|
||||
```
|
||||
|
||||
Packaging verification:
|
||||
|
||||
```text
|
||||
npm.cmd run pack
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Passed with escalated filesystem permission, generating `release/win-unpacked`.
|
||||
- A later non-escalated rerun was blocked by the sandbox while writing generated bundle output in the parent Scoreko project (`shared/dist`). That rerun failed before packaging because of sandbox filesystem permissions, not because of a build error.
|
||||
- A final escalated rerun could not be started because the approval system rejected the escalation. Typecheck, tests, lint, and doctor were run successfully around the packaging verification.
|
||||
|
||||
Sanity searches:
|
||||
|
||||
```text
|
||||
rg -n "\bany\b|update-manager|update-settings|update-utils|ActualizaciÃ|estÃ|versiÃ|nodeIntegration:\s*true|webSecurity:\s*false|ipcMain|ipcRenderer|contextBridge|preload" src scripts docs/refactor
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- No `any` was introduced in production or test source.
|
||||
- No legacy updater module references remain in `src`.
|
||||
- No touched Spanish update text is mojibaked.
|
||||
- No production IPC or preload surface exists.
|
||||
- No unsafe Electron window settings were introduced.
|
||||
- Remaining IPC/preload matches are documentation and the regression test that guards the zero-surface policy.
|
||||
@@ -1,190 +0,0 @@
|
||||
# Session Handoff
|
||||
|
||||
## Context
|
||||
|
||||
This handoff captures the agreed architecture direction for future refactor sessions. Do not restart architectural discovery from zero unless the codebase has changed substantially.
|
||||
|
||||
The previous analysis concluded that the codebase is fundamentally healthy. The refactor should be controlled and incremental, focused on lifecycle, updater safety, process shutdown, and explicit Electron security.
|
||||
|
||||
## Current Technical State
|
||||
|
||||
Known validation from the prior analysis:
|
||||
|
||||
```text
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run lint
|
||||
```
|
||||
|
||||
All passed at that time, with 42 tests passing.
|
||||
|
||||
## Source Of Truth Documents
|
||||
|
||||
Use these documents in order:
|
||||
|
||||
1. `docs/refactor/ARCHITECTURE_AUDIT.md`
|
||||
2. `docs/refactor/ARCHITECTURE_RULES.md`
|
||||
3. `docs/refactor/TARGET_ARCHITECTURE.md`
|
||||
4. `docs/refactor/MIGRATION_PLAN.md`
|
||||
5. `docs/refactor/SESSION_HANDOFF.md`
|
||||
|
||||
## High-Level Project Model
|
||||
|
||||
This is an Electron wrapper around a local NodeCG runtime.
|
||||
|
||||
Important facts:
|
||||
|
||||
- The app lives mostly in Electron main.
|
||||
- There is no custom renderer.
|
||||
- There is no preload.
|
||||
- There is no meaningful IPC.
|
||||
- Electron starts NodeCG locally.
|
||||
- Electron loads dashboards from local HTTP origins.
|
||||
- Runtime provisioning happens under Electron `userData`.
|
||||
- NodeCG is launched through the Electron binary with `ELECTRON_RUN_AS_NODE`.
|
||||
|
||||
Do not treat the absence of IPC or preload as a missing feature. It is currently a desirable security property.
|
||||
|
||||
## Preserve These Behaviors
|
||||
|
||||
- Runtime stored under `userData`.
|
||||
- Relaunch after first runtime installation when required.
|
||||
- Preservation of `cfg`, `db`, and `logs`.
|
||||
- Use of `ELECTRON_RUN_AS_NODE`.
|
||||
- Waiting for NodeCG HTTP readiness before dashboard load.
|
||||
- Existing tests for config, provisioning, navigation, process management, and updates.
|
||||
- Minimal Electron surface.
|
||||
- No IPC unless required.
|
||||
|
||||
## Main Risks To Address
|
||||
|
||||
### Lifecycle
|
||||
|
||||
`main.ts` currently mixes:
|
||||
|
||||
- App configuration.
|
||||
- Runtime preparation.
|
||||
- Window handling.
|
||||
- NodeCG startup.
|
||||
- Readiness waiting.
|
||||
- Update scheduling.
|
||||
- Shutdown.
|
||||
|
||||
Refactor target:
|
||||
|
||||
- Introduce `ApplicationController`.
|
||||
- Add explicit lifecycle states.
|
||||
- Keep `main.ts` or `bootstrap.ts` thin.
|
||||
|
||||
### Activation
|
||||
|
||||
Current risk:
|
||||
|
||||
- macOS activation can recreate a window and load a dashboard without proving NodeCG readiness.
|
||||
|
||||
Refactor target:
|
||||
|
||||
- Route activation through the same readiness-aware controller as startup.
|
||||
|
||||
### Updater
|
||||
|
||||
Current risk:
|
||||
|
||||
- Remote JSON and asset URLs need stronger validation.
|
||||
- Download and install behavior should be separated.
|
||||
- Spanish text contains visible encoding corruption.
|
||||
|
||||
Refactor target:
|
||||
|
||||
- Rewrite updater internals in small modules.
|
||||
- Validate metadata.
|
||||
- Validate URLs.
|
||||
- Use safe temporary paths.
|
||||
- Fix encoding.
|
||||
|
||||
### Process Shutdown
|
||||
|
||||
Current risk:
|
||||
|
||||
- Windows process-tree termination uses `taskkill`.
|
||||
- It is low risk because the PID is numeric, but platform behavior should be isolated.
|
||||
|
||||
Refactor target:
|
||||
|
||||
- Add `platform-process-killer.ts`.
|
||||
- Keep platform-specific commands out of NodeCG lifecycle orchestration.
|
||||
|
||||
### Electron Security
|
||||
|
||||
Current strengths:
|
||||
|
||||
- `nodeIntegration: false`
|
||||
- `contextIsolation: true`
|
||||
- `sandbox: true`
|
||||
- No preload.
|
||||
- No IPC.
|
||||
|
||||
Refactor target:
|
||||
|
||||
- Add explicit `webSecurity: true`.
|
||||
- Add permission denial by default.
|
||||
- Add devtools policy by environment.
|
||||
- Keep navigation allowlisted.
|
||||
|
||||
## Recommended Next Session
|
||||
|
||||
Start with Phase 1 from `MIGRATION_PLAN.md`.
|
||||
|
||||
Immediate next work:
|
||||
|
||||
1. Add lifecycle tests around current behavior.
|
||||
2. Cover first-run relaunch behavior.
|
||||
3. Cover loading window and main window order.
|
||||
4. Cover update scheduling.
|
||||
5. Cover shutdown idempotency.
|
||||
6. Cover activation readiness behavior.
|
||||
|
||||
Do not move files before these tests exist.
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- Do not re-architect around a renderer.
|
||||
- Do not introduce IPC proactively.
|
||||
- Do not rewrite the whole project.
|
||||
- Do not move folders before preserving behavior with tests.
|
||||
- Do not remove the managed NodeCG runtime strategy.
|
||||
- Do not delete user-owned runtime directories.
|
||||
- Do not broaden Electron permissions.
|
||||
|
||||
## Preferred Refactor Order
|
||||
|
||||
1. Tests around current lifecycle behavior.
|
||||
2. Pure path and URL extraction.
|
||||
3. `ApplicationController`.
|
||||
4. Shutdown service.
|
||||
5. Updater rewrite.
|
||||
6. Platform process-killer adapter.
|
||||
7. Electron security hardening.
|
||||
8. Script normalization.
|
||||
9. Folder reorganization.
|
||||
|
||||
## Completion Criteria For The Refactor
|
||||
|
||||
The refactor is successful when:
|
||||
|
||||
- Startup is controlled by a tested application controller.
|
||||
- Shutdown is idempotent.
|
||||
- Activation cannot load dashboards before NodeCG readiness.
|
||||
- Updater metadata and URLs are validated.
|
||||
- Downloads use safe paths and atomic finalization.
|
||||
- Process-tree termination is isolated by platform.
|
||||
- Browser windows declare secure settings explicitly.
|
||||
- Permission requests are denied by default.
|
||||
- No unnecessary IPC, preload, or renderer has been introduced.
|
||||
- Typecheck, tests, and lint pass.
|
||||
|
||||
## Reminder For Future Agents
|
||||
|
||||
This project does not need to become bigger to become safer.
|
||||
|
||||
Keep Electron small. Keep NodeCG ownership explicit. Keep remote update data untrusted. Keep the browser surface boring.
|
||||
@@ -1,420 +0,0 @@
|
||||
# Target Architecture
|
||||
|
||||
## Objective
|
||||
|
||||
The target architecture keeps the application small and explicit. Electron should remain a thin shell that owns desktop lifecycle, launches NodeCG, loads local dashboards, manages updates, and shuts down cleanly.
|
||||
|
||||
The target is not a full rewrite. It is a gradual extraction of responsibilities from `main.ts` into testable modules.
|
||||
|
||||
## Target Structure
|
||||
|
||||
```text
|
||||
src/main/
|
||||
app/
|
||||
bootstrap.ts
|
||||
application-controller.ts
|
||||
paths.ts
|
||||
shutdown-service.ts
|
||||
config/
|
||||
runtime-config.ts
|
||||
windows/
|
||||
window-service.ts
|
||||
navigation-policy.ts
|
||||
nodecg/
|
||||
runtime-provisioner.ts
|
||||
nodecg-process-service.ts
|
||||
platform-process-killer.ts
|
||||
updates/
|
||||
update-service.ts
|
||||
update-config.ts
|
||||
update-download.ts
|
||||
update-schema.ts
|
||||
logging/
|
||||
logger.ts
|
||||
shared/
|
||||
result.ts
|
||||
src/shared/
|
||||
types/
|
||||
config.ts
|
||||
```
|
||||
|
||||
`shared/result.ts` is optional and should be added only if it removes repeated error-handling noise.
|
||||
|
||||
`src/shared/types/config.ts` should contain only types that are genuinely shared across process boundaries or packages. It should not become a dumping ground.
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
```text
|
||||
Electron entrypoint
|
||||
-> bootstrap app identity, paths, lock, config
|
||||
-> create ApplicationController
|
||||
-> prepare managed NodeCG runtime
|
||||
-> relaunch if first install requires it
|
||||
-> create loading window
|
||||
-> start NodeCG process
|
||||
-> wait for HTTP readiness
|
||||
-> create or show main window
|
||||
-> load NodeCG dashboard URL
|
||||
-> close loading window
|
||||
-> schedule update checks
|
||||
```
|
||||
|
||||
Shutdown flow:
|
||||
|
||||
```text
|
||||
quit requested
|
||||
-> mark controller stopping
|
||||
-> stop update work if needed
|
||||
-> stop NodeCG process tree
|
||||
-> close windows
|
||||
-> allow app exit
|
||||
```
|
||||
|
||||
Activation flow:
|
||||
|
||||
```text
|
||||
activate requested
|
||||
-> if ready, create or show main window
|
||||
-> if not ready, route through readiness-aware startup
|
||||
-> never load dashboard before NodeCG readiness
|
||||
```
|
||||
|
||||
## Module Responsibilities
|
||||
|
||||
### `app/bootstrap.ts`
|
||||
|
||||
Owns Electron entrypoint side effects:
|
||||
|
||||
- Set app name.
|
||||
- Set app paths.
|
||||
- Acquire single-instance lock.
|
||||
- Register Electron app event handlers.
|
||||
- Instantiate services.
|
||||
- Delegate startup and shutdown to `ApplicationController`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Keep this file thin.
|
||||
- Do not put business logic here.
|
||||
- Do not make it responsible for update parsing, process killing, or window option construction.
|
||||
|
||||
### `app/application-controller.ts`
|
||||
|
||||
Owns high-level lifecycle state.
|
||||
|
||||
States:
|
||||
|
||||
```text
|
||||
idle
|
||||
preparing
|
||||
starting
|
||||
ready
|
||||
stopping
|
||||
stopped
|
||||
failed
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Coordinate runtime preparation.
|
||||
- Coordinate NodeCG startup and readiness.
|
||||
- Coordinate loading and main windows.
|
||||
- Coordinate update scheduling.
|
||||
- Coordinate activation.
|
||||
- Coordinate shutdown.
|
||||
|
||||
Rules:
|
||||
|
||||
- State transitions must be explicit.
|
||||
- Shutdown must be idempotent.
|
||||
- Activation must not bypass readiness.
|
||||
- Controller tests should not require real Electron where avoidable.
|
||||
|
||||
### `app/paths.ts`
|
||||
|
||||
Owns pure path construction:
|
||||
|
||||
- Electron `userData` derived paths.
|
||||
- Managed runtime path.
|
||||
- Safe temp locations.
|
||||
- Any path constants shared by startup and provisioning.
|
||||
|
||||
Rules:
|
||||
|
||||
- No Electron side effects.
|
||||
- No filesystem writes.
|
||||
- Pure functions only.
|
||||
|
||||
### `config/runtime-config.ts`
|
||||
|
||||
Owns runtime configuration:
|
||||
|
||||
- Parse environment variables.
|
||||
- Parse static config.
|
||||
- Define defaults.
|
||||
- Return typed runtime config.
|
||||
|
||||
Rules:
|
||||
|
||||
- Parse once.
|
||||
- Validate early.
|
||||
- Do not read environment variables throughout the application.
|
||||
|
||||
### `windows/window-service.ts`
|
||||
|
||||
Owns Electron window creation and window lifecycle.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Create loading window.
|
||||
- Create main window.
|
||||
- Apply secure `webPreferences`.
|
||||
- Apply devtools policy.
|
||||
- Register permission handlers.
|
||||
- Delegate navigation decisions to `navigation-policy`.
|
||||
|
||||
Rules:
|
||||
|
||||
- No NodeCG process logic.
|
||||
- No updater logic.
|
||||
- No runtime provisioning logic.
|
||||
|
||||
### `windows/navigation-policy.ts`
|
||||
|
||||
Owns pure navigation decisions.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Allow approved local NodeCG origins.
|
||||
- Block external navigation.
|
||||
- Block unexpected new-window attempts.
|
||||
- Normalize URL parsing.
|
||||
|
||||
Rules:
|
||||
|
||||
- Prefer `URL` parsing.
|
||||
- Keep policy testable without Electron.
|
||||
- Do not use broad string-prefix allow checks as the primary control.
|
||||
|
||||
### `nodecg/runtime-provisioner.ts`
|
||||
|
||||
Owns managed runtime installation and replacement.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Validate bundled runtime source.
|
||||
- Install runtime into `userData`.
|
||||
- Replace managed runtime safely.
|
||||
- Preserve `cfg`, `db`, and `logs`.
|
||||
- Report whether relaunch is needed.
|
||||
|
||||
Rules:
|
||||
|
||||
- User-owned data must not be deleted.
|
||||
- Runtime replacement must be predictable and test-covered.
|
||||
|
||||
### `nodecg/nodecg-process-service.ts`
|
||||
|
||||
Owns NodeCG process lifecycle.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Validate runtime before launch.
|
||||
- Start NodeCG with `ELECTRON_RUN_AS_NODE`.
|
||||
- Capture process output.
|
||||
- Wait for HTTP readiness.
|
||||
- Stop the process.
|
||||
- Delegate platform-specific process-tree termination.
|
||||
|
||||
Rules:
|
||||
|
||||
- Do not build platform kill commands here.
|
||||
- Do not create windows here.
|
||||
- Do not schedule updates here.
|
||||
|
||||
### `nodecg/platform-process-killer.ts`
|
||||
|
||||
Owns OS-specific process-tree termination.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Terminate a process tree on Windows.
|
||||
- Terminate a process tree on POSIX systems.
|
||||
- Validate process IDs before command construction.
|
||||
- Normalize process-kill errors.
|
||||
|
||||
Rules:
|
||||
|
||||
- Keep platform branches here.
|
||||
- Test command construction.
|
||||
- Keep inputs narrow and typed.
|
||||
|
||||
### `updates/update-service.ts`
|
||||
|
||||
Owns update orchestration.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Schedule update checks.
|
||||
- Fetch update metadata through a helper.
|
||||
- Validate metadata through schema helpers.
|
||||
- Select the correct platform asset.
|
||||
- Ask the user before installing.
|
||||
- Delegate download work.
|
||||
- Start installer only after validation and download success.
|
||||
|
||||
Rules:
|
||||
|
||||
- Do not trust remote metadata.
|
||||
- Do not mix dialogs with JSON parsing.
|
||||
- Do not mix installer execution with download streaming.
|
||||
|
||||
### `updates/update-config.ts`
|
||||
|
||||
Owns update settings:
|
||||
|
||||
- Feed URL.
|
||||
- Current app version.
|
||||
- Platform selection.
|
||||
- Development-mode behavior.
|
||||
|
||||
Rules:
|
||||
|
||||
- Keep production and development behavior explicit.
|
||||
- Do not silently downgrade security in production.
|
||||
|
||||
### `updates/update-download.ts`
|
||||
|
||||
Owns download behavior:
|
||||
|
||||
- Validate URL protocol.
|
||||
- Download to a safe temp path.
|
||||
- Write atomically.
|
||||
- Return a typed result.
|
||||
|
||||
Rules:
|
||||
|
||||
- No dialogs.
|
||||
- No installer execution.
|
||||
- No remote metadata interpretation.
|
||||
|
||||
### `updates/update-schema.ts`
|
||||
|
||||
Owns runtime validation:
|
||||
|
||||
- Update metadata shape.
|
||||
- Asset shape.
|
||||
- Version field presence.
|
||||
- URL field validity before download selection.
|
||||
|
||||
Rules:
|
||||
|
||||
- Unknown remote JSON must be validated before use.
|
||||
- Invalid metadata must fail closed.
|
||||
|
||||
### `logging/logger.ts`
|
||||
|
||||
Optional thin logging boundary.
|
||||
|
||||
Rules:
|
||||
|
||||
- Add only if it improves consistency.
|
||||
- Do not introduce a large logging framework.
|
||||
- Keep logs useful for startup, process, update, and shutdown diagnostics.
|
||||
|
||||
## Electron Decisions
|
||||
|
||||
- Keep Electron main as the only privileged process.
|
||||
- Keep Node.js unavailable to web content.
|
||||
- Keep custom renderer absent unless a concrete feature requires one.
|
||||
- Keep preload absent unless a desktop API must cross into web content.
|
||||
- Treat windows as untrusted web surfaces even when loading local NodeCG dashboards.
|
||||
|
||||
Required `BrowserWindow` security posture:
|
||||
|
||||
```text
|
||||
nodeIntegration: false
|
||||
contextIsolation: true
|
||||
sandbox: true
|
||||
webSecurity: true
|
||||
```
|
||||
|
||||
Additional decisions:
|
||||
|
||||
- Deny permissions by default.
|
||||
- Control devtools by environment.
|
||||
- Block external navigation by default.
|
||||
- Block unexpected new windows.
|
||||
- Review CSP options for NodeCG-hosted content, but do not break dashboards to satisfy theoretical policy.
|
||||
|
||||
## IPC Decisions
|
||||
|
||||
Current target:
|
||||
|
||||
- No IPC.
|
||||
- No preload.
|
||||
- No exposed desktop API.
|
||||
|
||||
Future IPC, if needed:
|
||||
|
||||
```text
|
||||
src/main/ipc/
|
||||
channels.ts
|
||||
register-handlers.ts
|
||||
validators.ts
|
||||
src/shared/ipc/
|
||||
types.ts
|
||||
```
|
||||
|
||||
IPC must be:
|
||||
|
||||
- Explicitly justified by a product requirement.
|
||||
- Channel allowlisted.
|
||||
- Payload validated at runtime.
|
||||
- Typed at compile time.
|
||||
- Narrow in capability.
|
||||
|
||||
IPC must not expose:
|
||||
|
||||
- Raw `ipcRenderer`.
|
||||
- Filesystem primitives.
|
||||
- Process primitives.
|
||||
- Shell execution.
|
||||
- Update installation primitives.
|
||||
- Arbitrary NodeCG process controls.
|
||||
|
||||
## Security Decisions
|
||||
|
||||
Security controls to preserve:
|
||||
|
||||
- No Node.js in web content.
|
||||
- Context isolation enabled.
|
||||
- Sandbox enabled.
|
||||
- No IPC surface by default.
|
||||
- Local navigation only.
|
||||
|
||||
Security controls to add:
|
||||
|
||||
- Explicit `webSecurity: true`.
|
||||
- Permission handler that denies by default.
|
||||
- Explicit devtools policy.
|
||||
- Strong update metadata validation.
|
||||
- Strong update asset URL validation.
|
||||
- Safe temporary download paths.
|
||||
- Atomic download finalization.
|
||||
- Platform process-kill isolation.
|
||||
|
||||
## What This Architecture Should Feel Like
|
||||
|
||||
Future maintainers should be able to answer these questions quickly:
|
||||
|
||||
- Where does startup happen?
|
||||
- Where is NodeCG launched?
|
||||
- Where is readiness checked?
|
||||
- Where are windows created?
|
||||
- Where is navigation allowed or denied?
|
||||
- Where are updates validated?
|
||||
- Where is shutdown coordinated?
|
||||
- Where does platform-specific process killing live?
|
||||
|
||||
If a future change makes those answers harder, it is moving against the target architecture.
|
||||
+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",
|
||||
|
||||
+16
-7
@@ -34,6 +34,7 @@
|
||||
"dist:mac": "npm run build && npm run rebuild:native && electron-builder --mac"
|
||||
},
|
||||
"build": {
|
||||
"beforePack": "./scripts/before-pack.mjs",
|
||||
"appId": "com.scoreko.desktop",
|
||||
"productName": "Scoreko",
|
||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
|
||||
@@ -53,6 +54,10 @@
|
||||
{
|
||||
"from": "static",
|
||||
"to": "static"
|
||||
},
|
||||
{
|
||||
"from": ".env",
|
||||
"to": ".env"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
@@ -77,6 +82,7 @@
|
||||
"signAndEditExecutable": true
|
||||
},
|
||||
"nsis": {
|
||||
"include": "static/installer.nsh",
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"artifactName": "${productName}-setup-${version}.${ext}",
|
||||
@@ -85,7 +91,8 @@
|
||||
"installerHeaderIcon": "static/icons/icon.ico",
|
||||
"shortcutName": "Scoreko",
|
||||
"useZip": false,
|
||||
"deleteAppDataOnUninstall": true
|
||||
"deleteAppDataOnUninstall": true,
|
||||
"createDesktopShortcut": "always"
|
||||
},
|
||||
"compression": "normal"
|
||||
},
|
||||
@@ -93,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// scripts/beforePack.mjs
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default async function () {
|
||||
const src = path.resolve(__dirname, '../static/installSection.nsh');
|
||||
const dest = path.resolve(__dirname, '../node_modules/app-builder-lib/templates/nsis/installSection.nsh');
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log('✅ installSection.nsh parcheado');
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { NodecgProcessManager } from "../nodecg/process-manager";
|
||||
import { PreparedNodecgRuntime } from "../nodecg/runtime-provisioner";
|
||||
import { PreparedNodecgRuntime } from "../nodecg/runtime-setup";
|
||||
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;
|
||||
@@ -13,6 +13,7 @@ export type ApplicationWindow = {
|
||||
isDestroyed: () => boolean;
|
||||
isMinimized: () => boolean;
|
||||
loadURL: (url: string) => Promise<unknown>;
|
||||
loadFile: (filePath: string) => Promise<unknown>;
|
||||
restore: () => void;
|
||||
show: () => void;
|
||||
};
|
||||
@@ -52,6 +53,7 @@ export type ApplicationController = {
|
||||
focusExistingWindow: () => void;
|
||||
getState: () => ApplicationState;
|
||||
launch: () => Promise<void>;
|
||||
showErrorScreen: (error: unknown) => Promise<void>;
|
||||
stopNodecgGracefully: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -59,7 +61,7 @@ export function createApplicationController({
|
||||
appConfig,
|
||||
appVersion,
|
||||
deps,
|
||||
isPackaged,
|
||||
isPackaged: _isPackaged,
|
||||
isWindows,
|
||||
paths,
|
||||
}: ApplicationControllerConfig): ApplicationController {
|
||||
@@ -119,6 +121,13 @@ export function createApplicationController({
|
||||
deps.setAppUserModelId(appConfig.userModelId);
|
||||
}
|
||||
|
||||
mainWindow = deps.createMainWindow();
|
||||
loadingWindow = deps.createLoadingWindow();
|
||||
|
||||
await loadingWindow.loadFile(paths.staticLoadingHtmlPath);
|
||||
loadingWindow.show();
|
||||
await sleep(50);
|
||||
|
||||
state = "preparing";
|
||||
const preparedRuntime = deps.prepareRuntime({
|
||||
sourceRuntimePath: paths.sourceNodecgRuntimePath,
|
||||
@@ -131,9 +140,6 @@ export function createApplicationController({
|
||||
|
||||
nodecgManager = deps.createNodecgProcessManager(preparedRuntime.runtimePath);
|
||||
|
||||
mainWindow = deps.createMainWindow();
|
||||
loadingWindow = deps.createLoadingWindow();
|
||||
|
||||
state = "starting";
|
||||
await startNodecg();
|
||||
|
||||
@@ -142,9 +148,6 @@ export function createApplicationController({
|
||||
return;
|
||||
}
|
||||
|
||||
await loadingWindow.loadURL(paths.loadingDashboardUrl);
|
||||
loadingWindow.show();
|
||||
|
||||
const loadingShownAt = now();
|
||||
|
||||
if (!mainWindow) {
|
||||
@@ -174,7 +177,7 @@ export function createApplicationController({
|
||||
} catch (error) {
|
||||
state = "failed";
|
||||
launchPromise = null;
|
||||
closeLoadingWindow();
|
||||
await showErrorScreen(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -204,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import { app, BrowserWindow } from "electron";
|
||||
import path from "node:path";
|
||||
|
||||
import { getRuntimeConfig, loadEnvFile, AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { showFatalError, log } from "../errors/error-presenter";
|
||||
import { showFatalError, log } from "../errors/error-handler";
|
||||
import { logger } from "../logging/logger";
|
||||
import { createNodecgProcessManager } from "../nodecg/process-manager";
|
||||
import { prepareUserNodecgRuntime } from "../nodecg/runtime-provisioner";
|
||||
import { prepareUserNodecgRuntime } from "../nodecg/runtime-setup";
|
||||
import { scheduleUpdateCheck } from "../updates/update-service";
|
||||
import { createLoadingWindow, createMainWindow } from "../windows/window-service";
|
||||
import { createApplicationController } from "./application-controller";
|
||||
@@ -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,7 +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 {
|
||||
@@ -62,7 +63,7 @@ export function getApplicationPaths({
|
||||
}: {
|
||||
appConfig: Pick<
|
||||
AppRuntimeConfig,
|
||||
"bundleName" | "loadingDashboardRoute" | "mainDashboardRoute" | "nodecgPort" | "userDataDirectoryName"
|
||||
"bundleName" | "mainDashboardRoute" | "nodecgPort" | "userDataDirectoryName"
|
||||
>;
|
||||
appDataPath: string;
|
||||
compiledMainDir: string;
|
||||
@@ -77,6 +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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export type AppRuntimeConfig = {
|
||||
title: string;
|
||||
@@ -8,7 +9,6 @@ export type AppRuntimeConfig = {
|
||||
nodecgPort: string;
|
||||
bundleName: string;
|
||||
mainDashboardRoute: string;
|
||||
loadingDashboardRoute: string;
|
||||
loadDelayMs: number;
|
||||
startupTimeoutMs: number;
|
||||
nodecgKillTimeoutMs: number;
|
||||
@@ -23,13 +23,9 @@ const MIN_TCP_PORT = 1;
|
||||
const MAX_TCP_PORT = 65535;
|
||||
|
||||
export function loadEnvFile(envFilePath: string): void {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
throw new Error(
|
||||
`Archivo de configuración obligatorio no encontrado: ${envFilePath}\n\nPor favor, crea un archivo .env basado en .env.example en la raíz de la aplicación.`,
|
||||
);
|
||||
}
|
||||
const resolvedPath = resolveEnvFilePath(envFilePath);
|
||||
try {
|
||||
process.loadEnvFile(envFilePath);
|
||||
process.loadEnvFile(resolvedPath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error al leer el archivo de configuración .env: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -37,6 +33,22 @@ export function loadEnvFile(envFilePath: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEnvFilePath(envFilePath: string): string {
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
return envFilePath;
|
||||
}
|
||||
|
||||
const dir = path.dirname(envFilePath);
|
||||
const fallbackPath = path.join(dir, ".env.example");
|
||||
if (fs.existsSync(fallbackPath)) {
|
||||
return fallbackPath;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Archivo de configuración obligatorio no encontrado: ${envFilePath}\n\nPor favor, crea un archivo .env basado en .env.example en la raíz de la aplicación.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getRuntimeConfig(): AppRuntimeConfig {
|
||||
return {
|
||||
title: getRequiredEnv("SCOREKO_APP_TITLE"),
|
||||
@@ -46,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 = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
||||
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { NODE_RUNTIME_NAME } from "../constants";
|
||||
import { killProcessTree } from "./platform-process-killer";
|
||||
import { killProcessTree } from "./process-killer";
|
||||
|
||||
type NodecgProcessManagerConfig = {
|
||||
isDev: boolean;
|
||||
@@ -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,
|
||||
|
||||
@@ -23,11 +23,13 @@ type RuntimeProvisionerDeps = {
|
||||
recursive: true;
|
||||
force: true;
|
||||
dereference: true;
|
||||
filter: (sourcePath: string) => boolean;
|
||||
filter?: (sourcePath: string) => boolean;
|
||||
},
|
||||
) => unknown;
|
||||
readFileSync: (filePath: string) => string | Buffer;
|
||||
writeFileSync: (filePath: string, content: string) => unknown;
|
||||
statSync: (filePath: string) => { isDirectory: () => boolean };
|
||||
symlinkSync: (target: string, path: string, type: "junction") => unknown;
|
||||
};
|
||||
|
||||
export type PreparedNodecgRuntime = {
|
||||
@@ -84,6 +86,8 @@ function resolveDeps(deps?: Partial<RuntimeProvisionerDeps>): RuntimeProvisioner
|
||||
cpSync: deps?.cpSync ?? fs.cpSync,
|
||||
readFileSync: deps?.readFileSync ?? fs.readFileSync,
|
||||
writeFileSync: deps?.writeFileSync ?? fs.writeFileSync,
|
||||
statSync: deps?.statSync ?? fs.statSync,
|
||||
symlinkSync: deps?.symlinkSync ?? fs.symlinkSync,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,16 +154,20 @@ function installManagedRuntime(
|
||||
deps.rmSync(path.join(targetRuntimePath, entry), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
deps.cpSync(sourceRuntimePath, targetRuntimePath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
dereference: true,
|
||||
filter: (sourcePath) => {
|
||||
const relativePath = path.relative(sourceRuntimePath, sourcePath);
|
||||
const firstSegment = relativePath.split(path.sep)[0];
|
||||
return !WRITABLE_NODECG_DIRS.includes(firstSegment as (typeof WRITABLE_NODECG_DIRS)[number]);
|
||||
},
|
||||
});
|
||||
for (const entry of MANAGED_RUNTIME_ENTRIES) {
|
||||
const sourcePath = path.join(sourceRuntimePath, entry);
|
||||
const targetPath = path.join(targetRuntimePath, entry);
|
||||
|
||||
if (!deps.existsSync(sourcePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deps.statSync(sourcePath).isDirectory()) {
|
||||
deps.symlinkSync(sourcePath, targetPath, "junction");
|
||||
} else {
|
||||
deps.cpSync(sourcePath, targetPath, { recursive: true, force: true, dereference: true });
|
||||
}
|
||||
}
|
||||
|
||||
const sourceRuntime = readJson(path.join(sourceRuntimePath, ".scoreko-runtime.json"), deps);
|
||||
deps.writeFileSync(
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isRecord, readNonEmptyString } from "../utils/unknown-values";
|
||||
|
||||
export type GiteaReleaseAsset = {
|
||||
type GiteaReleaseAsset = {
|
||||
name: string;
|
||||
browserDownloadUrl: string;
|
||||
size?: number;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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";
|
||||
import { resolveAppIconPath } from "./icon-path";
|
||||
import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "./navigation-policy";
|
||||
import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "./navigation";
|
||||
|
||||
type WindowServiceDependencies = {
|
||||
appConfig: AppRuntimeConfig;
|
||||
@@ -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",
|
||||
|
||||
@@ -35,6 +35,10 @@ class MockWindow implements ApplicationWindow {
|
||||
this.events.push(`${this.name}:load:${url}`);
|
||||
}
|
||||
|
||||
async loadFile(filePath: string): Promise<void> {
|
||||
this.events.push(`${this.name}:loadFile:${filePath}`);
|
||||
}
|
||||
|
||||
restore(): void {
|
||||
this.events.push(`${this.name}:restore`);
|
||||
this.minimized = false;
|
||||
@@ -53,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,
|
||||
@@ -86,7 +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({
|
||||
@@ -129,14 +133,15 @@ test("ApplicationController preserves startup ordering and schedules updates aft
|
||||
assert.equal(controller.getState(), "ready");
|
||||
assert.deepEqual(events, [
|
||||
"set-app-user-model-id",
|
||||
"prepare-runtime",
|
||||
"create-manager",
|
||||
"create-main",
|
||||
"create-loading",
|
||||
`loading:loadFile:${paths.staticLoadingHtmlPath}`,
|
||||
"loading:show",
|
||||
"sleep:50",
|
||||
"prepare-runtime",
|
||||
"create-manager",
|
||||
"start-nodecg",
|
||||
"wait-nodecg",
|
||||
`loading:load:${paths.loadingDashboardUrl}`,
|
||||
"loading:show",
|
||||
`main:load:${paths.mainDashboardUrl}`,
|
||||
"main:show",
|
||||
"loading:close",
|
||||
@@ -157,7 +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: () => {
|
||||
@@ -189,13 +195,14 @@ test("ApplicationController directly launches packaged app after runtime install
|
||||
|
||||
assert.equal(controller.getState(), "ready");
|
||||
assert.deepEqual(events, [
|
||||
"create-manager",
|
||||
"create-main",
|
||||
"create-loading",
|
||||
"loading:loadFile:/app/static/loading.html",
|
||||
"loading:show",
|
||||
"sleep:50",
|
||||
"create-manager",
|
||||
"start-nodecg",
|
||||
"wait-nodecg",
|
||||
"loading:load:http://localhost:9090/loading",
|
||||
"loading:show",
|
||||
"main:load:http://localhost:9090/main",
|
||||
"main:show",
|
||||
"loading:close",
|
||||
@@ -216,7 +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),
|
||||
@@ -256,7 +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>;
|
||||
@@ -38,10 +38,21 @@ function createFakeFs(initialPaths: string[] = [], initialFiles: Record<string,
|
||||
cpSync: (from: string, to: string) => {
|
||||
state.copied.push({ from: path.normalize(from), to: path.normalize(to) });
|
||||
state.paths.add(path.normalize(to));
|
||||
state.paths.add(path.join(path.normalize(to), "index.js"));
|
||||
state.paths.add(path.join(path.normalize(to), "package.json"));
|
||||
state.paths.add(path.join(path.normalize(to), "node_modules", "nodecg", "dist", "server", "bootstrap.js"));
|
||||
state.paths.add(path.join(path.normalize(to), "bundles", "scoreko-dev", "package.json"));
|
||||
},
|
||||
statSync: (filePath: string) => ({
|
||||
isDirectory: () => {
|
||||
const normalized = path.normalize(filePath);
|
||||
return normalized.endsWith("node_modules") || normalized.endsWith("bundles");
|
||||
},
|
||||
}),
|
||||
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")) {
|
||||
state.paths.add(path.join(path.normalize(linkPath), "nodecg", "dist", "server", "bootstrap.js"));
|
||||
} else if (target.endsWith("bundles")) {
|
||||
state.paths.add(path.join(path.normalize(linkPath), "scoreko-dev", "package.json"));
|
||||
}
|
||||
},
|
||||
readFileSync: (filePath: string) => state.files.get(path.normalize(filePath)) ?? "{}",
|
||||
writeFileSync: (filePath: string, content: string) => {
|
||||
@@ -57,7 +68,9 @@ function getSourcePaths(source: string) {
|
||||
source,
|
||||
path.join(source, "index.js"),
|
||||
path.join(source, "package.json"),
|
||||
path.join(source, "node_modules"),
|
||||
path.join(source, "node_modules", "nodecg", "dist", "server", "bootstrap.js"),
|
||||
path.join(source, "bundles"),
|
||||
path.join(source, "bundles", "scoreko-dev", "package.json"),
|
||||
path.join(source, ".scoreko-runtime.json"),
|
||||
];
|
||||
@@ -81,7 +94,7 @@ test("prepareUserNodecgRuntime copies the packaged runtime into userData", () =>
|
||||
|
||||
assert.equal(preparedRuntime.runtimePath, path.join(userData, "nodecg"));
|
||||
assert.equal(preparedRuntime.installed, true);
|
||||
assert.equal(state.copied.length, 1);
|
||||
assert.equal(state.copied.length, 4);
|
||||
assert.ok(state.paths.has(path.join(userData, "nodecg", "cfg")));
|
||||
assert.ok(state.paths.has(path.join(userData, "nodecg", "db")));
|
||||
assert.ok(state.paths.has(path.join(userData, "nodecg", "logs")));
|
||||
@@ -150,7 +163,7 @@ test("prepareUserNodecgRuntime refreshes managed files when the app version chan
|
||||
});
|
||||
|
||||
assert.equal(preparedRuntime.installed, true);
|
||||
assert.equal(state.copied.length, 1);
|
||||
assert.equal(state.copied.length, 4);
|
||||
assert.ok(state.removed.includes(path.join(target, "node_modules")));
|
||||
assert.ok(state.removed.includes(path.join(target, "bundles")));
|
||||
assert.ok(!state.removed.includes(path.join(target, "db")));
|
||||
@@ -190,7 +203,7 @@ test("prepareUserNodecgRuntime refreshes managed files when the source runtime w
|
||||
});
|
||||
|
||||
assert.equal(preparedRuntime.installed, true);
|
||||
assert.equal(state.copied.length, 1);
|
||||
assert.equal(state.copied.length, 4);
|
||||
assert.ok(state.removed.includes(path.join(target, "bundles")));
|
||||
assert.ok(!state.removed.includes(path.join(target, "cfg")));
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,110 @@
|
||||
!include installer.nsh
|
||||
|
||||
InitPluginsDir
|
||||
|
||||
${IfNot} ${Silent}
|
||||
SetDetailsPrint both
|
||||
${endif}
|
||||
|
||||
StrCpy $appExe "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
|
||||
|
||||
# must be called before uninstallOldVersion
|
||||
!insertmacro setLinkVars
|
||||
|
||||
!ifdef ONE_CLICK
|
||||
!ifdef HEADER_ICO
|
||||
File /oname=$PLUGINSDIR\installerHeaderico.ico "${HEADER_ICO}"
|
||||
!endif
|
||||
${IfNot} ${Silent}
|
||||
!ifdef HEADER_ICO
|
||||
SpiderBanner::Show /MODERN /ICON "$PLUGINSDIR\installerHeaderico.ico"
|
||||
!else
|
||||
SpiderBanner::Show /MODERN
|
||||
!endif
|
||||
|
||||
FindWindow $0 "#32770" "" $hwndparent
|
||||
FindWindow $0 "#32770" "" $hwndparent $0
|
||||
GetDlgItem $0 $0 1000
|
||||
SendMessage $0 ${WM_SETTEXT} 0 "STR:$(installing)"
|
||||
|
||||
StrCpy $1 $hwndparent
|
||||
System::Call 'user32::ShutdownBlockReasonCreate(${SYSTYPE_PTR}r1, w "$(installing)")'
|
||||
${endif}
|
||||
!insertmacro CHECK_APP_RUNNING
|
||||
!else
|
||||
${ifNot} ${UAC_IsInnerInstance}
|
||||
!insertmacro CHECK_APP_RUNNING
|
||||
${endif}
|
||||
!endif
|
||||
|
||||
Var /GLOBAL keepShortcuts
|
||||
StrCpy $keepShortcuts "false"
|
||||
!insertMacro setIsTryToKeepShortcuts
|
||||
${if} $isTryToKeepShortcuts == "true"
|
||||
ReadRegStr $R1 SHELL_CONTEXT "${INSTALL_REGISTRY_KEY}" KeepShortcuts
|
||||
|
||||
${if} $R1 == "true"
|
||||
${andIf} ${FileExists} "$appExe"
|
||||
StrCpy $keepShortcuts "true"
|
||||
${endIf}
|
||||
${endif}
|
||||
|
||||
!insertmacro uninstallOldVersion SHELL_CONTEXT
|
||||
!insertmacro handleUninstallResult SHELL_CONTEXT
|
||||
|
||||
${if} $installMode == "all"
|
||||
!insertmacro uninstallOldVersion HKEY_CURRENT_USER
|
||||
!insertmacro handleUninstallResult HKEY_CURRENT_USER
|
||||
${endIf}
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!ifdef UNINSTALLER_ICON
|
||||
File /oname=uninstallerIcon.ico "${UNINSTALLER_ICON}"
|
||||
!endif
|
||||
|
||||
!insertmacro installApplicationFiles
|
||||
!insertmacro registryAddInstallInfo
|
||||
!insertmacro addStartMenuLink $keepShortcuts
|
||||
!insertmacro addDesktopLink $keepShortcuts
|
||||
|
||||
${if} ${FileExists} "$newStartMenuLink"
|
||||
StrCpy $launchLink "$newStartMenuLink"
|
||||
${else}
|
||||
StrCpy $launchLink "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
|
||||
${endIf}
|
||||
|
||||
!ifmacrodef registerFileAssociations
|
||||
!insertmacro registerFileAssociations
|
||||
!endif
|
||||
|
||||
!ifmacrodef customInstall
|
||||
!insertmacro customInstall
|
||||
!endif
|
||||
|
||||
!macro doStartApp
|
||||
# otherwise app window will be in background
|
||||
HideWindow
|
||||
!insertmacro StartApp
|
||||
!macroend
|
||||
|
||||
!ifdef ONE_CLICK
|
||||
# https://github.com/electron-userland/electron-builder/pull/3093#issuecomment-403734568
|
||||
!ifdef RUN_AFTER_FINISH
|
||||
${ifNot} ${Silent}
|
||||
${orIf} ${isForceRun}
|
||||
!insertmacro doStartApp
|
||||
${endIf}
|
||||
!else
|
||||
${if} ${isForceRun}
|
||||
!insertmacro doStartApp
|
||||
${endIf}
|
||||
!endif
|
||||
!insertmacro quitSuccess
|
||||
!else
|
||||
# for assisted installer run only if silent, because assisted installer has run after finish option
|
||||
${if} ${isForceRun}
|
||||
${andIf} ${Silent}
|
||||
!insertmacro doStartApp
|
||||
${endIf}
|
||||
!endif
|
||||
@@ -0,0 +1,4 @@
|
||||
!macro customHeader
|
||||
ShowInstDetails hide
|
||||
ShowUninstDetails hide
|
||||
!macroend
|
||||
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline';">
|
||||
<title>Scoreko</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #121212;
|
||||
color: #f5f5f5;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
.loading-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.q-spinner {
|
||||
animation: q-spin 2s linear infinite;
|
||||
transform-origin: center center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
color: #1976d2;
|
||||
}
|
||||
.q-spinner circle {
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0;
|
||||
animation: q-spin-dash 1.5s ease-in-out infinite;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
@keyframes q-spin {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes q-spin-dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 89, 200;
|
||||
stroke-dashoffset: -35px;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 89, 200;
|
||||
stroke-dashoffset: -124px;
|
||||
}
|
||||
}
|
||||
.quote {
|
||||
max-width: 520px;
|
||||
margin-top: 16px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
.status {
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading-page">
|
||||
<div class="loading-content">
|
||||
<svg class="q-spinner" viewBox="25 25 50 50">
|
||||
<circle cx="50" cy="50" r="20" fill="none" stroke="currentColor" stroke-width="5" stroke-miterlimit="10"></circle>
|
||||
</svg>
|
||||
<div class="quote" id="quoteText"></div>
|
||||
<div class="status">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const loadQuotes = [
|
||||
"Complaining about Paul's damage",
|
||||
'Nerfing Gigas',
|
||||
'Mashing hopkick',
|
||||
'Sidestepping your electric',
|
||||
'Punishing hellsweep with 1,1,2',
|
||||
'Emailing Harada',
|
||||
];
|
||||
const randomIndex = Math.floor(Math.random() * loadQuotes.length);
|
||||
document.getElementById('quoteText').textContent = loadQuotes[randomIndex];
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user