From 93aeca9d974925defc9abf4a1e24802d9d333e20 Mon Sep 17 00:00:00 2001 From: kaiwren986 <153867712+kaiwren986@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:58:27 -0400 Subject: [PATCH] Add Living Universe v0.2.2 server bridge overlay --- CHANGELOG.md | 15 + CONTRIBUTING.md | 14 +- Install-Server-Bridge.bat | 8 + README.md | 68 +- Uninstall-Server-Bridge.bat | 8 + app/globals.css | 4 +- bridge/v0.2.2-evejs-v0.12.3.1/README.md | 40 + .../x-command/assets/index-CBiB6eNy.css | 2 + .../assets/x-command/assets/index-CG2pp9Po.js | 9 + .../files/server/assets/x-command/index.html | 17 + .../server/scripts/verifyXCommandBridge.js | 566 +++++++++++ .../server/scripts/verifyXCommandLocalUi.js | 369 ++++++++ .../server/scripts/verifyXCommandUniverse.js | 491 ++++++++++ .../src/_secondary/xCommand/httpAuth.js | 156 ++++ .../server/src/_secondary/xCommand/server.js | 365 ++++++++ .../src/_secondary/xCommand/staticUi.js | 138 +++ .../src/services/xCommand/xCommandAuth.js | 493 ++++++++++ .../services/xCommand/xCommandChatCommand.js | 140 +++ .../xCommand/xCommandIndustrialAdapter.js | 883 ++++++++++++++++++ .../src/services/xCommand/xCommandRegistry.js | 71 ++ .../xCommand/xCommandUniverseAdapter.js | 403 ++++++++ bridge/v0.2.2-evejs-v0.12.3.1/manifest.json | 56 ++ docs/DEVELOPMENT.md | 19 +- docs/INTEGRATION.md | 60 +- docs/STATUS.md | 90 +- eslint.config.mjs | 1 + package-lock.json | 4 +- package.json | 7 +- server-reference/v0.12.3.1/README.md | 5 +- tools/x-command-bridge.mjs | 628 +++++++++++++ 30 files changed, 5050 insertions(+), 80 deletions(-) create mode 100644 Install-Server-Bridge.bat create mode 100644 Uninstall-Server-Bridge.bat create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/README.md create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/assets/index-CBiB6eNy.css create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/assets/index-CG2pp9Po.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/index.html create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandBridge.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandLocalUi.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandUniverse.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/httpAuth.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/server.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/staticUi.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandAuth.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandChatCommand.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandIndustrialAdapter.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandRegistry.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandUniverseAdapter.js create mode 100644 bridge/v0.2.2-evejs-v0.12.3.1/manifest.json create mode 100644 tools/x-command-bridge.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c7434..dc70c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.2.0 - 2026-07-31 + +- Adds an installable, SHA-256-locked server bridge for public X-Eve Living + Universe v0.2.2 / EveJS v0.12.3.1. +- Installs the current same-origin UI and API listener, secure `/x` link and + session flow, industrial and universe adapters, auth persistence, and chat + wiring. +- Enables native loopback use and Docker port/environment wiring while + preserving the game-port-plus-proxy Play readiness contract. +- Adds idempotent verification, automatic failure rollback, and an uninstall + path that restores the exact public baseline. +- Records focused server, live listener/static/API, package-integrity, and + clean install/rollback results; the release remains prerelease pending the + full native/Docker real-client and sustained-soak gate. + ## 0.1.0 - 2026-07-31 Initial public developer handoff. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28a0f99..6a10cdc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,9 +8,11 @@ best changes are small, testable, and explicit about compatibility. - Add live adapters for Contracts, Production, Salvage, or broader Logistics. - Improve narrow-screen and keyboard map behavior. - Remove or replace unused site scaffold code without breaking either build. -- Port the server reference onto the current public Living Universe release. -- Build a clean installer, rollback path, and Docker/native test matrix for - that overlay. +- Extend the current bridge with adapters for preview-only modules. +- Add the remaining Docker/native first-start, second-start, real-client, and + sustained-soak automation. +- Prepare a new hash-locked overlay when Living Universe publishes a later + release. ## Pull requests @@ -20,8 +22,10 @@ best changes are small, testable, and explicit about compatibility. 3. Run `npm ci` and `npm run check`. 4. Add or update tests for behavior and security changes. 5. Do not commit tokens, local configuration, game stores, upstream server - trees, generated bundles, or personal deployment metadata. + trees, or personal deployment metadata. The one generated artifact that is + intentional is the versioned static UI under the bridge payload. For server work, state the exact EveJS and Living Universe versions tested. -Do not call a reference overlay installable until it passes the gate in +For bridge changes, update the manifest hashes and repeat the clean install, +focused verification, listener smoke, and exact rollback gate described in `docs/INTEGRATION.md`. diff --git a/Install-Server-Bridge.bat b/Install-Server-Bridge.bat new file mode 100644 index 0000000..51a4c5e --- /dev/null +++ b/Install-Server-Bridge.bat @@ -0,0 +1,8 @@ +@echo off +setlocal +if "%~1"=="" ( + echo Usage: Install-Server-Bridge.bat C:\path\to\LivingUniverse + exit /b 2 +) +node "%~dp0tools\x-command-bridge.mjs" install --target "%~f1" +exit /b %ERRORLEVEL% diff --git a/README.md b/README.md index 017bfb2..3bd696a 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,10 @@ operations screen, industrial crew controls, and a searchable universe map. ![X Command](public/og.png) -> **Development handoff:** the browser app is verified, but X Command is not -> included in the current public X-Eve Living Universe v0.2.2 release. A live -> connection requires the server bridge to be ported and packaged for that -> release. The code under `server-reference/` is a tested development -> reference, not a supported installer. +> **Current release:** v0.2.0 includes an installable server-bridge overlay for +> the public X-Eve Living Universe v0.2.2 / EveJS v0.12.3.1 release. It is an +> exact-version overlay: the installer verifies the public baseline before it +> changes anything and keeps a recoverable backup for uninstall. ## Current state @@ -21,7 +20,9 @@ operations screen, industrial crew controls, and a searchable universe map. | Automated interface tests | 20 passing | | Industrial crew UI | Implemented against the documented bridge contract | | Universe map UI | Implemented against the documented bridge contract | -| Current public Living Universe v0.2.2 | Bridge not included; live linking does not work yet | +| Server bridge for Living Universe v0.2.2 | Included as an exact-version installable overlay | +| Native listener smoke test | Passed on `127.0.0.1:26500` | +| Docker wiring | Included; requires rebuilding the local `evejs-local` image | | Hosted community service | Not provided | The detailed, evidence-based matrix is in [Project status](docs/STATUS.md). @@ -49,6 +50,47 @@ That command lints the source, performs the production build, runs the 20 contract/rendering tests, and builds the standalone EveJS client into `dist/evejs-client/`. +## Install the server bridge + +Install the public +[X-Eve Living Universe v0.2.2](https://github.com/kaiwren986/evejs-living-universe/releases/tag/v0.2.2) +first, then stop its server and market processes. From this repository run: + +```bat +Install-Server-Bridge.bat "C:\path\to\your\LivingUniverse" +``` + +The installer: + +- accepts only the exact public v0.2.2 source baseline; +- backs up every shared file before changing it; +- installs the current static UI, API, auth store, universe and industrial + adapters, focused verifiers, and `/x` chat wiring; +- enables the loopback listener at `http://127.0.0.1:26500/` for native mode; +- exposes the same listener through Docker without making it a Play launcher + readiness requirement; and +- runs the three focused server verifiers before reporting success. + +Native users can then start the server normally. Docker users must rebuild the +local image once after install: + +```powershell +Set-Location "C:\path\to\your\LivingUniverse" +docker compose build init +docker compose up --detach +``` + +In game, type `/x` to create a short-lived, one-time character link. To remove +the bridge, stop the server and run: + +```bat +Uninstall-Server-Bridge.bat "C:\path\to\your\LivingUniverse" +``` + +Docker users should rebuild `evejs-local` again after uninstall. See +[EveJS integration](docs/INTEGRATION.md) for configuration, security, remote +HTTPS, verification, and exact compatibility details. + ## How live mode is intended to work The current interface is designed to be bundled into the same EveJS server @@ -62,7 +104,7 @@ and localhost HTTP are accepted; a separately hosted UI calling a different API domain is not the current supported design. See [EveJS integration](docs/INTEGRATION.md) for the API contract, security -rules, required wiring, and the current compatibility gap. +rules, overlay behavior, and remaining compatibility limits. ## Implemented surfaces @@ -85,8 +127,12 @@ subset represented by the current contract. - `app/` - interface, bridge bootstrap, and universe map. - `evejs-client/` - standalone client entry point used by `build:evejs`. - `tests/` - rendering, security, industrial, and universe contract tests. +- `bridge/v0.2.2-evejs-v0.12.3.1/` - installable, hash-locked server bridge + overlay and current static UI bundle. +- `tools/x-command-bridge.mjs` - install, verify, package-integrity, and + rollback command. - `server-reference/v0.12.3.1/` - last verified server-side X Command modules - and verifiers; development reference only. + and verifiers; retained as historical development reference. - `legacy-server/v0.12.2-patch-series/` - historical patch series; do not apply it to v0.12.3.1. - `docs/` - status, integration, and development guidance. @@ -95,9 +141,9 @@ subset represented by the current contract. X Command was designed beside [X-Eve Living Universe](https://github.com/kaiwren986/evejs-living-universe). -That repository is the supported public Living Universe patch. Keep the two -projects separate until an X Command overlay has its own clean-install and -runtime gate. +That repository remains the supported public Living Universe patch. X Command +is distributed separately so its listener stays optional and does not alter +the Living Universe release or launcher readiness gate. ## License and independence diff --git a/Uninstall-Server-Bridge.bat b/Uninstall-Server-Bridge.bat new file mode 100644 index 0000000..ebf97a3 --- /dev/null +++ b/Uninstall-Server-Bridge.bat @@ -0,0 +1,8 @@ +@echo off +setlocal +if "%~1"=="" ( + echo Usage: Uninstall-Server-Bridge.bat C:\path\to\LivingUniverse + exit /b 2 +) +node "%~dp0tools\x-command-bridge.mjs" uninstall --target "%~f1" +exit /b %ERRORLEVEL% diff --git a/app/globals.css b/app/globals.css index 92f297e..f1fee4e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,4 +1,6 @@ -@import "tailwindcss"; +@import "tailwindcss" source(none); +@source "./"; +@source "../evejs-client"; :root { --void: #05080a; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/README.md b/bridge/v0.2.2-evejs-v0.12.3.1/README.md new file mode 100644 index 0000000..306e579 --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/README.md @@ -0,0 +1,40 @@ +# X Command server bridge 0.2.0 + +This overlay targets exactly: + +- X-Eve Living Universe v0.2.2 +- EveJS v0.12.3.1 +- Windows native or Docker deployment from the public v0.2.2 package + +Run the repository-root `Install-Server-Bridge.bat` with the Living Universe +install directory. The Node installer validates SHA-256 fingerprints from +`manifest.json`, backs up every shared file under +`_local/x-command/backups/`, copies the bridge modules and static UI, adds only +the required shared hooks, enables the listener, and runs the focused server +verifiers. + +The overlay adds: + +- the loopback HTTP listener and same-origin static UI on port 26500; +- one-time `/x` link exchange and HttpOnly session handling; +- character-scoped session, industrial, topology, snapshot, and route APIs; +- `xCommandAuth` game-store persistence; +- `/x`, `/x status`, and `/x reset` chat dispatch; and +- Docker environment and port wiring. + +It does not make port 26500 part of `Play.bat` readiness. Game port 26000 and +proxy port 26002 remain sufficient, including when Docker keeps runtime +telemetry inside the container. This prevents X Command from causing a second +native server launch. + +For Docker, rebuild `evejs-local` after install so the changed source and +assets enter the image: + +```powershell +docker compose build init +docker compose up --detach +``` + +The uninstall command restores the exact backed-up source and removes only +files owned by this overlay. It refuses a modified installed bridge unless the +operator explicitly supplies `--force` to the Node command. diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/assets/index-CBiB6eNy.css b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/assets/index-CBiB6eNy.css new file mode 100644 index 0000000..40cd8fd --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/assets/index-CBiB6eNy.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */ +@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.hidden{display:none}}:root{--void:#05080a;--ink:#091014;--panel:#0c151ae3;--panel-strong:#0e191ff5;--line:#7cb1c02b;--line-bright:#74d3e86b;--text:#d9e4e7;--muted:#7f969e;--blue:#55d5ee;--blue-soft:#8be6f6;--green:#72e2b2;--amber:#e2b15a;--red:#e07878;--sidebar-width:224px}*{box-sizing:border-box}html,body{background:var(--void);min-height:100%;color:var(--text);font-family:var(--font-geist-sans), Arial, sans-serif;margin:0}button,select,input{font:inherit}button{color:inherit}.app-shell{grid-template-columns:var(--sidebar-width) minmax(0, 1fr);background:radial-gradient(circle at 76% 2%,#1b64792b,#0000 34rem),linear-gradient(125deg,#06090c 0%,#071015 52%,#040608 100%);min-height:100vh;display:grid;position:relative;overflow:hidden}.space-backdrop{pointer-events:none;opacity:.68;background-image:radial-gradient(circle at 82% 14%,#64daf080 0 1px,#0000 2px),radial-gradient(circle at 66% 27%,#ffffff61 0 1px,#0000 1.7px),radial-gradient(circle at 91% 58%,#ffffff45 0 1px,#0000 1.6px),radial-gradient(circle at 41% 76%,#48a1bb6b 0 1px,#0000 2px),linear-gradient(113deg,#0000 38%,#2b6e800a 48%,#0000 61%);background-size:390px 390px,270px 270px,460px 460px,350px 350px,cover;position:fixed;inset:0}.space-backdrop:before,.space-backdrop:after{content:"";filter:blur(50px);border-radius:50%;position:absolute}.space-backdrop:before{background:#0f586b1a;width:500px;height:180px;top:110px;right:-90px;transform:rotate(-24deg)}.space-backdrop:after{background:#1946521c;width:420px;height:240px;bottom:-120px;left:24%}.sidebar{z-index:5;border-right:1px solid var(--line);background:linear-gradient(#080e12fa,#060b0ef2);flex-direction:column;min-height:100vh;display:flex;position:relative;box-shadow:15px 0 50px #00000038}.brand{border-bottom:1px solid var(--line);align-items:center;gap:13px;height:60px;padding:0 16px;display:flex}.brand-mark{width:36px;height:36px;color:var(--blue-soft);background:#3799b214;border:1px solid #64daf07a;place-items:center;display:grid;position:relative;transform:rotate(45deg);box-shadow:inset 0 0 16px #4ac3dc17,0 0 18px #2f9eb814}.brand-mark span{font:700 17px var(--font-geist-mono), monospace;transform:rotate(-45deg)}.brand strong{letter-spacing:.13em;font-size:14px;display:block}.brand small{color:#698089;font:600 8px var(--font-geist-mono), monospace;letter-spacing:.19em;margin-top:3px;display:block}.sidebar nav{flex:1;min-height:0;padding:8px;overflow-y:auto}.nav-group{margin:0 0 8px}.nav-group>p{color:#566a72;font:650 9px var(--font-geist-mono), monospace;letter-spacing:.16em;text-transform:uppercase;margin:0;padding:7px 10px 4px}.nav-group button{color:#879ba2;text-align:left;cursor:pointer;background:0 0;border:0;border-left:2px solid #0000;grid-template-columns:28px minmax(0,1fr) auto;align-items:center;gap:9px;width:100%;min-height:36px;padding:4px 10px;transition:color .14s,background .14s,border-color .14s;display:grid;position:relative}.nav-group button:hover{color:#d8e4e7;background:#69b7c90d}.nav-group button.active{color:#e7f4f7;border-left-color:var(--blue);background:linear-gradient(90deg,#37a7c02e,#37a7c006)}.nav-group button.active:after{content:"";background:#55d5ee61;width:1px;height:58%;position:absolute;right:0}.nav-icon{color:#7fa2ab;width:25px;height:25px;font:650 8px var(--font-geist-mono), monospace;clip-path:polygon(20% 0,100% 0,100% 80%,80% 100%,0 100%,0 20%);background:0 0;border:1px solid #0000;place-items:center;display:grid}.nav-group button.active .nav-icon{color:var(--blue-soft);background:#3db1cb1a;border-color:#55d5ee80}.nav-group button em{color:#98b4bc;min-width:20px;font:normal 600 9px var(--font-geist-mono), monospace;text-align:center;background:#4776821c;border:1px solid #71a6b430;border-radius:20px;padding:2px 6px}.sidebar-footer{border-top:1px solid var(--line);background:#04090c85;grid-template-columns:8px minmax(0,1fr);align-items:center;gap:10px;min-height:62px;padding:12px 18px;display:grid}.sidebar-status>i{background:#647981;border-radius:50%;width:7px;height:7px;box-shadow:0 0 10px #64798159}.sidebar-status>span{flex-direction:column;gap:3px;min-width:0;display:flex}.sidebar-status strong{color:#9cafb5;font:650 9px var(--font-geist-mono), monospace;letter-spacing:.12em;text-transform:uppercase}.sidebar-status small{color:#647981;font:8px var(--font-geist-mono), monospace;text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.sidebar-status.connection-online>i{background:var(--green);box-shadow:0 0 12px #72e2b2b8}.sidebar-status.connection-online strong{color:#9be7c6}.sidebar-status.connection-offline>i{background:var(--amber);box-shadow:0 0 12px #e2b15a9e}.sidebar-status.connection-offline strong{color:#d8b775}.sidebar-status.connection-connecting>i{background:var(--blue);box-shadow:0 0 12px #55d5ee9e}.sidebar-status.connection-preview>i{background:#71868d}.workspace{z-index:2;grid-template-rows:60px minmax(0,1fr);min-width:0;height:100vh;display:grid;position:relative}.topbar{border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);background:#060b0ed6;justify-content:space-between;align-items:center;gap:14px;padding:0 22px;display:flex}.chrome-title{color:#b8cbd0;min-width:0;font:650 12px var(--font-geist-mono), monospace;letter-spacing:.12em;text-transform:uppercase;white-space:nowrap;text-overflow:ellipsis;flex:1;margin:0;line-height:1;overflow:hidden}.chrome-actions{align-items:center;gap:12px;height:100%;margin-left:auto;display:flex}.chrome-refresh{min-height:32px;padding-inline:13px}.top-status{align-items:stretch;height:100%;display:flex}.top-status>div{border-left:0;flex-direction:column;justify-content:center;min-width:auto;padding:0 12px;display:flex}.top-status span,.top-status small{color:#647a81;font-size:9px}.top-status strong{color:#cbdadd;margin-top:3px;font-size:11px;font-weight:550}.security{color:var(--blue);margin-left:6px;font-style:normal}.top-status .pilot-chip{border-left:1px solid var(--line);flex-direction:row;justify-content:flex-start;align-items:center;gap:10px;min-width:160px}.pilot-chip>span:last-child{flex-direction:column;display:flex}.pilot-chip strong{margin-top:0}.avatar{width:32px;height:32px;font:600 10px var(--font-geist-mono), monospace;background:linear-gradient(145deg,#357d8e47,#10242b73);border:1px solid #59bbd05c;place-items:center;display:grid;color:var(--blue-soft)!important}.content-scroller{scrollbar-color:#67a4b261 #00000026;overflow:auto}.screen{width:min(1420px,100%);margin:0 auto;padding:24px 28px 52px;animation:.22s ease-out screen-in}@keyframes screen-in{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}.module-header{justify-content:space-between;align-items:center;gap:20px;margin-bottom:18px;display:flex}.eyebrow{color:#60828b;font:600 9px var(--font-geist-mono), monospace;letter-spacing:.11em;text-transform:uppercase;margin:0 0 5px}h1,h2,p{text-wrap:pretty}h1{color:#e2ecee;letter-spacing:-.045em;margin:0;font-size:clamp(27px,2.4vw,34px);font-weight:520;line-height:1.06}h2{color:#d6e3e6;letter-spacing:-.025em;margin:0;font-size:17px;font-weight:540}.module-copy{color:#81959c;max-width:720px;margin:0;font-size:13px;line-height:1.55}.header-actions,.button-row{align-items:center;gap:8px;display:flex}.bridge-banner{color:#7d9299;background:#091115b3;border:1px solid #71a5b124;align-items:center;gap:11px;min-height:42px;margin:-13px 0 12px;padding:8px 12px;font-size:9px;display:flex}.bridge-banner.bridge-live{background:#235f4912;border-color:#72e2b233}.bridge-banner.bridge-error{background:#6a491b12;border-color:#e2b15a33}.button{color:#bfd0d4;letter-spacing:.03em;cursor:pointer;clip-path:polygon(6px 0,100% 0,100% calc(100% - 6px),calc(100% - 6px) 100%,0 100%,0 6px);border:1px solid #0000;min-height:36px;padding:0 15px;font-size:10px;font-weight:620;transition:filter .14s,border-color .14s,background .14s}.button:hover{filter:brightness(1.18)}.button:disabled,select:disabled{cursor:not-allowed;opacity:.46;filter:none}button:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--blue-soft);outline-offset:2px}.button.primary{color:#dbf8fc;background:linear-gradient(#349ab26b,#1b5b6b59);border-color:#55d5ee7d;box-shadow:inset 0 0 18px #5bdaf20d}.button.ghost{background:#4d788314;border-color:#7facb73d}.button.danger{color:#dca3a3;background:#93363614;border-color:#d5666638}.text-button{color:#65cfe5;cursor:pointer;background:0 0;border:0;padding:5px 0;font-size:10px}.text-button:hover{color:#a9effc}.panel{background:linear-gradient(135deg, #3f778409, transparent 40%), var(--panel);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);clip-path:polygon(0 0,calc(100% - 10px) 0,100% 10px,100% 100%,10px 100%,0 calc(100% - 10px));border:1px solid #7cb1c01c;position:relative;box-shadow:0 8px 24px #00000029}.panel:before{content:"";opacity:0;background:#55d5ee66;width:2px;height:32px;position:absolute;top:19px;left:-1px}.active-operation:before,.universe-target-panel:before{opacity:1}.panel-topline{justify-content:space-between;align-items:flex-start;gap:20px;display:flex}.panel-topline.compact{gap:10px}.status-pill{color:#9ab0b5;font:600 8px var(--font-geist-mono), monospace;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;background:0 0;border:1px solid #0000;align-items:center;gap:7px;padding:5px 8px;display:inline-flex}.status-pill i{background:#71888f;border-radius:50%;width:5px;height:5px;box-shadow:0 0 8px}.status-green{color:#7edab1}.status-green i{background:var(--green)}.status-blue{color:#75d4e8}.status-blue i{background:var(--blue)}.status-amber{color:#d1aa68}.status-amber i{background:var(--amber)}.status-pill.status-amber{background:#6a491b12;border-color:#e2b15a33}.metric-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:10px;display:grid}.metric{border:1px solid var(--line);background:#0a1216bf;min-height:104px;padding:16px 17px;position:relative;overflow:hidden}.metric:after{content:"";border:1px solid #4fa7bb1c;width:80px;height:80px;position:absolute;bottom:-28px;right:-14px;transform:rotate(45deg)}.metric-label{color:#71878e;font:600 9px var(--font-geist-mono), monospace;letter-spacing:.09em;text-transform:uppercase}.metric-value{color:#bfeef6;font:520 25px var(--font-geist-mono), monospace;letter-spacing:-.05em;margin:10px 0 3px}.metric-green .metric-value{color:#a9e8cc}.metric-amber .metric-value{color:#dfbd82}.metric-detail{color:#63777e;font-size:9px}.overview-grid{grid-template-columns:minmax(0,1.7fr) minmax(320px,.9fr);gap:10px;display:grid}.active-operation,.signal-feed,.contract-strip,.ledger-panel{padding:21px}.wing-visual{background:#03070957;border:1px solid #5d94a11c;grid-template-columns:minmax(0,1fr) 66px minmax(0,1fr);align-items:center;margin:18px 0 13px;display:grid}.ship-portrait{background:radial-gradient(circle at 50% 42%,#3d84952e,#0000 43%),#060b0e;min-width:0;height:158px;position:relative;overflow:hidden}.ship-portrait img{object-fit:cover;filter:saturate(.68)contrast(1.1)brightness(.88);width:100%;height:100%;transition:transform .28s,filter .28s;display:block}.ship-portrait:hover img{filter:saturate(.8)contrast(1.12)brightness(.98);transform:scale(1.025)}.ship-image-unavailable{color:#526971;font:650 8px var(--font-geist-mono), monospace;letter-spacing:.15em;place-items:center;display:grid;position:absolute;inset:0}.image-fade{background:linear-gradient(#0000 38%,#050a0deb);position:absolute;inset:0}.ship-name{color:#c9dcdf;font:550 11px var(--font-geist-mono), monospace;position:absolute;bottom:11px;left:13px}.link-line{background:linear-gradient(90deg,#081013f2,#0d1c21db,#081013f2);place-items:center;height:100%;display:grid;position:relative}.link-line:before,.link-line:after{content:"";background:#55d5ee4f;width:38%;height:1px;position:absolute;top:50%}.link-line:before{left:0}.link-line:after{right:0}.link-line span{z-index:1;color:#74bfce;font:7px var(--font-geist-mono), monospace;background:#0b1519;padding:5px}.operation-readout,.crew-stats{background:#6aa3af17;border:1px solid #6296a117;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;display:grid}.operation-readout>div,.crew-stats>div{background:#070d10e6;padding:10px 12px}.operation-readout span,.crew-stats span,.cargo-line span,.command-band span,.command-band small{color:#637a81;font-size:8px;display:block}.operation-readout strong,.crew-stats strong,.cargo-line strong{color:#afc1c5;margin-top:4px;font-size:10px;font-weight:560;display:block}.cargo-line{margin:15px 0}.cargo-line>div{justify-content:space-between;align-items:center;display:flex}.progress{background:#678e971f;height:4px;margin-top:7px;overflow:hidden}.progress span{height:100%;display:block;box-shadow:0 0 12px}.progress-blue{color:var(--blue);background:var(--blue)}.progress-green{color:var(--green);background:var(--green)}.progress-amber{color:var(--amber);background:var(--amber)}.signal-list{margin-top:13px}.signal-list button{text-align:left;cursor:pointer;background:0 0;border:0;border-top:1px solid #74a5b11a;grid-template-columns:32px minmax(0,1fr) auto;align-items:center;gap:10px;width:100%;padding:12px 0;display:grid}.signal-list button:hover strong{color:var(--blue-soft)}.signal-list strong,.signal-list small{display:block}.signal-list strong{color:#afc1c5;font-size:10px;font-weight:560}.signal-list small{color:#63777d;margin-top:4px;font-size:9px}.signal-list time{color:#536970;font:8px var(--font-geist-mono), monospace}.signal-icon{width:30px;height:30px;font:650 8px var(--font-geist-mono), monospace;border:1px solid #5f909b40;place-items:center;display:grid}.signal-icon.amber{color:#dfb464;background:#e2b15a0f;border-color:#e2b15a47}.signal-icon.blue{color:#66cee3;background:#55d5ee0d;border-color:#55d5ee40}.signal-icon.green{color:#75d7ae;background:#72e2b20d;border-color:#72e2b240}.contract-strip{margin-top:10px}.table-wrap{margin-top:14px;overflow-x:auto}table{border-collapse:collapse;width:100%;min-width:650px}th{color:#526a72;text-align:left;font:600 8px var(--font-geist-mono), monospace;letter-spacing:.1em;text-transform:uppercase;border-bottom:1px solid #699eaa21;padding:8px 10px}td{color:#879ca2;border-bottom:1px solid #699eaa14;padding:12px 10px;font-size:10px}tbody tr{transition:background .14s}tbody tr:hover{background:#4fabbf0b}tbody tr:last-child td{border-bottom:0}td:first-child{color:#bac9cc}.row-marker{border:1px solid var(--blue);width:5px;height:5px;margin-right:9px;display:inline-block;transform:rotate(45deg)}.table-link{color:#bac9cc;text-align:left;cursor:pointer;background:0 0;border:0;padding:0}.table-link:hover{color:var(--blue-soft)}.isk{color:#9bd5c0}.mono{font-family:var(--font-geist-mono), monospace}.command-band{background:#0b171cde;border:1px solid #5ea8b833;grid-template-columns:minmax(230px,1fr) minmax(280px,.8fr) auto;align-items:end;gap:20px;margin-bottom:10px;padding:15px 17px;display:grid}.command-band strong{color:#c3d4d7;margin:4px 0;font-size:12px;font-weight:550;display:block}.command-label{font:650 8px var(--font-geist-mono), monospace;letter-spacing:.12em;text-transform:uppercase;color:#68cde1!important}.command-band label>span{margin-bottom:6px}.demo-disclosure{color:#a99370;background:#6f4b1914;border:1px solid #e2b15a47;align-items:center;gap:12px;margin-bottom:10px;padding:10px 13px;font-size:9px;display:flex}.demo-disclosure strong{color:var(--amber);font:700 8px var(--font-geist-mono), monospace;letter-spacing:.14em;flex:none}.action-constraint{color:#99866b;background:#6f4b1912;border:1px solid #e2b15a3d;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:12px;margin-bottom:10px;padding:10px 13px;font-size:9px;display:grid}.action-constraint strong{color:#d4ab68;font:650 8px var(--font-geist-mono), monospace;letter-spacing:.08em;text-transform:uppercase}.destination-readout{margin-top:5px}.industrial-command-band{grid-template-columns:minmax(210px,1fr) minmax(170px,.7fr) minmax(190px,.7fr) auto}.crew-commission-panel{margin-bottom:10px;padding:15px 17px}.crew-commission-topline{grid-template-columns:minmax(260px,1fr) minmax(250px,.72fr) auto;align-items:end;gap:20px;display:grid}.crew-commission-intro>strong,.crew-commission-intro>small{display:block}.crew-commission-intro>strong{color:#c3d4d7;margin:4px 0;font-size:12px;font-weight:550}.crew-commission-intro>small,.crew-commission-topline label>span{color:#637a81;font-size:8px}.crew-commission-topline label>span{margin-bottom:6px;display:block}.crew-package-facts{background:#6aa3af17;border:1px solid #6296a117;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;margin:14px 0 0;display:grid}.crew-package-facts>div{background:#070d10e6;min-width:0;padding:10px 12px}.crew-package-facts dt,.crew-package-facts dd{margin:0}.crew-package-facts dt{color:#637a81;font-size:8px}.crew-package-facts dd{color:#afc1c5;margin-top:4px;font-size:10px;font-weight:560;line-height:1.35}.crew-package-facts .crew-package-total dd{color:#9bd5c0}.crew-pricing-note{color:#71868c;border-top:1px solid #689eaa1f;grid-template-columns:auto minmax(0,1fr);gap:10px;margin:11px 0 0;padding-top:10px;font-size:8px;line-height:1.45;display:grid}.crew-pricing-note strong{color:#9cc7d1;font:650 8px var(--font-geist-mono), monospace;letter-spacing:.06em;text-transform:uppercase}.crew-pricing-note.pricing-quoted strong{color:#d4ab68}.crew-package-empty{color:#6e848b;border:1px dashed #689eaa29;margin-top:13px;padding:12px;font-size:9px}.industrial-operations{grid-template-columns:minmax(240px,.42fr) minmax(0,1fr);gap:10px;margin-bottom:25px;display:grid}.system-browser,.site-command-panel{min-width:0;min-height:410px;padding:19px}.site-command-panel{background:radial-gradient(circle at 84% 20%,#48b0c729,#0000 34%),linear-gradient(100deg,#040a0dfa 0%,#040a0df0 48%,#040a0dc2 100%)}.system-list{gap:7px;margin-top:17px;display:grid}.system-list button{color:#8da1a7;text-align:left;cursor:pointer;background:#050b0e70;border:1px solid #679ba724;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:12px;width:100%;min-height:66px;padding:10px 12px;display:grid}.system-list button:hover{background:#2e7a8c14;border-color:#55d5ee47}.system-list button.selected{box-shadow:inset 2px 0 var(--blue);background:linear-gradient(90deg,#3293a82b,#1c48530d);border-color:#55d5ee75}.system-list button span:first-child strong,.system-list button span:first-child small,.system-count strong,.system-count small{display:block}.system-list button span:first-child strong{color:#c1d1d4;font-size:11px;font-weight:580}.system-list button span:first-child small{color:#657b82;font:8px var(--font-geist-mono), monospace;margin-top:4px}.system-count{text-align:center;border-left:1px solid #679ba721;min-width:38px;padding-left:10px}.system-count strong{color:#9bc9d2;font:600 12px var(--font-geist-mono), monospace}.system-count small{color:#5f747b;margin-top:3px;font-size:7px}.site-command-heading{border-bottom:1px solid #699eaa1f;grid-template-columns:minmax(0,1fr) minmax(220px,.55fr);align-items:end;gap:22px;padding-bottom:16px;display:grid}.site-command-heading p:not(.eyebrow){color:#667b82;margin:6px 0 0;font-size:9px}.site-command-heading label>span{color:#637a81;margin-bottom:6px;font-size:8px;display:block}.site-list{gap:7px;margin-top:14px;display:grid}.site-row{background:#040a0d70;border:1px solid #699eaa21;grid-template-columns:34px minmax(170px,1fr) minmax(160px,.8fr) auto auto;align-items:center;gap:12px;min-height:76px;padding:11px 12px;display:grid}.site-row.site-assigned{background:#225b6914;border-color:#55b5cd3d}.site-marker{color:#72cddd;width:32px;height:32px;font:650 8px var(--font-geist-mono), monospace;background:#2c7d9114;border:1px solid #55d5ee47;place-items:center;display:grid}.site-available .site-marker{color:var(--green);background:#2e795b12;border-color:#72e2b247}.site-identity>span,.site-identity>strong,.site-identity>small,.site-assignment>span,.site-assignment>strong{display:block}.site-identity>span,.site-assignment>span{color:#5d737a;font:7px var(--font-geist-mono), monospace;letter-spacing:.08em;text-transform:uppercase}.site-identity>strong,.site-assignment>strong{color:#b7c8cb;margin-top:4px;font-size:10px;font-weight:560}.site-identity>small{color:#51666d;font:7px var(--font-geist-mono), monospace;margin-top:4px}.telemetry-empty{color:#667c83;background:#050a0d47;border:1px dashed #689eaa2b;align-content:center;min-height:250px;margin-top:15px;padding:20px;display:grid}.telemetry-empty strong,.telemetry-empty span{display:block}.telemetry-empty strong{color:#a9bbbf;font-size:12px;font-weight:560}.telemetry-empty span{max-width:420px;margin-top:7px;font-size:9px;line-height:1.5}.site-empty{min-height:280px}.section-heading{justify-content:space-between;align-items:end;gap:18px;margin:0 2px 11px;display:flex}.section-heading>span{color:#637a81;font:8px var(--font-geist-mono), monospace;letter-spacing:.08em;text-transform:uppercase}.crew-roster{margin-top:3px}select{color:#b8c9cd;background:#0a1418;border:1px solid #6fa8b538;border-radius:0;outline:none;width:100%;height:36px;padding:0 11px}select:focus{border-color:#55d5ee8c}.crew-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.crew-card{grid-template-columns:minmax(180px,.7fr) minmax(260px,1.3fr);min-height:315px;display:grid}.crew-card .ship-portrait{height:100%;min-height:315px}.crew-body{flex-direction:column;padding:20px;display:flex}.pilot-name{color:#688087;margin:5px 0 0;font-size:9px}.crew-stats{grid-template-columns:.8fr 1fr .8fr;margin-top:20px}.crew-body .cargo-line{margin-top:auto}.crew-body .button-row{flex-wrap:wrap}.crew-actions .button{flex:115px}.crew-control-note{color:#a18c6d;background:#724e1b14;border-left:2px solid #e2b15a8c;flex:1 0 100%;padding:8px 10px;font-size:8px;line-height:1.45}.telemetry-state{min-height:320px}.ledger-panel{margin-top:10px}.empty-state{align-content:center;justify-items:start;min-height:230px;padding:28px;display:grid}.empty-state p:not(.eyebrow),.ledger-unavailable{color:#6e848b;font-size:10px;line-height:1.5}.ledger-unavailable{background:#050a0d4d;border:1px dashed #689eaa29;margin-top:14px;padding:18px}.ledger-grid{background:#669ca817;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;margin-top:14px;display:grid}.ledger-grid>div{background:#0a1115;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;padding:13px;display:grid;position:relative}.ledger-grid span{color:#506971;font:8px var(--font-geist-mono), monospace}.ledger-grid strong{color:#a8babf;font-size:10px;font-weight:550}.ledger-grid em{color:#83cfb4;font:normal 9px var(--font-geist-mono), monospace}.ledger-grid small{color:#5d737a;grid-column:2/-1;font-size:8px}.universe-screen{width:min(1600px,100%)}.universe-disclosure,.universe-live-disclosure{margin:0 0 10px}.universe-live-disclosure{color:#7d969d;background:#1e5c460f;border:1px solid #72e2b22e;align-items:center;gap:11px;min-height:42px;padding:8px 12px;font-size:9px;display:flex}.universe-live-disclosure .button{margin-left:auto}.universe-live-disclosure.universe-disclosure-error{background:#6a491b12;border-color:#e2b15a3d}.universe-command-grid{grid-template-columns:minmax(520px,1.55fr) minmax(310px,.55fr);gap:8px;display:grid}.universe-map-panel{min-width:0;box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none;clip-path:none;background:0 0;border:0;padding:0}.universe-map-panel:before{display:none}.universe-map-heading{justify-content:space-between;align-items:flex-start;gap:15px;min-height:32px;padding:0 2px 8px;display:flex}.universe-map-heading h2{font-size:13px}.universe-map-heading p{color:#74919a;font:9px var(--font-geist-mono), monospace;margin:3px 0 0}.universe-count{color:#74919a;font:600 9px var(--font-geist-mono), monospace;letter-spacing:.08em;text-transform:uppercase}.universe-map-stage{background:linear-gradient(#5ea1b109 1px,#0000 1px) 0 0/32px 32px,linear-gradient(90deg,#5ea1b109 1px,#0000 1px) 0 0/32px 32px,radial-gradient(circle at 50% 45%,#1b64761f,#0000 58%) 0 0/cover,#050a0d 0 0/cover;border:1px solid #69a8b62b;height:clamp(480px,58vh,690px);min-height:480px;position:relative;overflow:hidden}.universe-map-stage:before,.universe-map-stage:after{content:"";z-index:1;pointer-events:none;position:absolute}.universe-map-stage:before{border:1px solid #5ad1e809;inset:0;box-shadow:inset 0 0 70px #0000007a}.universe-map-stage:after{border-top:1px solid #62bfd31a;border-left:1px solid #62bfd31a;width:18px;height:18px;top:50%;left:50%;transform:translate(-9px,-9px)}.universe-canvas{z-index:2;touch-action:none;cursor:grab;width:100%;height:100%;display:block;position:absolute;inset:0}.universe-canvas:active{cursor:grabbing}.universe-canvas:focus-visible{outline:2px solid var(--blue-soft);outline-offset:-3px}.universe-map-controls{z-index:3;background:#060e12e6;border:1px solid #60aebf33;grid-template-columns:32px 32px 45px;display:grid;position:absolute;top:12px;right:12px}.universe-map-controls button{color:#82cddd;height:30px;font:600 10px var(--font-geist-mono), monospace;cursor:pointer;background:0 0;border:0;border-right:1px solid #60aebf24;padding:0}.universe-map-controls button:last-child{border-right:0;font-size:8px}.universe-map-controls button:hover{color:#d9f6fb;background:#4499ad21}.universe-map-legend{z-index:3;color:#6e8991;font:9px var(--font-geist-mono), monospace;text-transform:uppercase;background:#060e12db;border:1px solid #60aebf1f;flex-wrap:wrap;gap:10px 15px;padding:8px 10px;display:flex;position:absolute;bottom:12px;left:12px}.universe-map-legend span{align-items:center;gap:6px;display:flex}.universe-map-legend i{border-radius:50%;width:6px;height:6px}.universe-map-legend .legend-high{background:#57d7ef}.universe-map-legend .legend-low{background:#e5b257}.universe-map-legend .legend-null{background:#e06969}.universe-map-legend .legend-owned{background:0 0;border-bottom:7px solid #76ebb7;border-left:4px solid #0000;border-right:4px solid #0000;border-radius:0;width:0;height:0}.universe-command-rail{flex-direction:column;gap:8px;min-width:0;display:flex}.universe-asset-panel,.universe-target-panel,.universe-route-panel,.universe-system-browser{padding:14px}.universe-asset-panel{min-height:190px}.universe-target-panel{flex:1;min-height:260px}.universe-asset-list{scrollbar-color:#67a4b24d transparent;border-top:1px solid #699eaa1c;max-height:340px;margin-top:8px;overflow:auto}.universe-asset-list button,.universe-system-list button{color:#8da3aa;text-align:left;cursor:pointer;background:0 0;border:0;border-bottom:1px solid #699eaa1a;align-items:center;width:100%;display:grid}.universe-asset-list button{grid-template-columns:22px minmax(0,1fr);gap:0 8px;min-height:59px;padding:9px 8px}.universe-asset-list button:hover,.universe-system-list button:hover{background:#4da8bc0e}.universe-asset-list button.selected,.universe-system-list button.selected{color:#d2e5e9;background:linear-gradient(90deg,#359fb721,#359fb706);box-shadow:inset 2px 0 #55d5ee}.owned-marker{color:var(--green);text-align:center;text-shadow:0 0 10px #72e2b280;grid-row:1/3;font-size:10px}.universe-asset-list strong,.universe-asset-list small,.universe-asset-list em,.universe-system-list strong,.universe-system-list small{display:block}.universe-asset-list strong,.universe-system-list strong{color:inherit;font-size:10px;font-weight:570}.universe-asset-list small,.universe-system-list small{color:#60767d;margin-top:3px;font-size:9px}.universe-asset-list em{color:#6c8991;font:normal 9px var(--font-geist-mono), monospace;text-transform:uppercase;grid-column:2;margin-top:5px}.universe-operation-node{border-bottom:1px solid #699eaa14;position:relative}.universe-operation-node>.universe-operation-button{border-bottom:0;grid-template-columns:22px minmax(0,1fr) 16px;min-height:52px}.universe-operation-node>.universe-operation-button.selected{color:#d8eef2;background:linear-gradient(90deg,#359fb726,#359fb706)}.universe-operation-button>em{color:#78a8b3;text-align:center;grid-area:1/3/3;align-self:center;margin:0;font-size:13px}.operation-marker{color:var(--green);text-align:center;text-shadow:0 0 10px #72e2b28c;grid-row:1/3;font-size:8px}.universe-crew-tree{background:#050d116b;border-left:1px solid #56c0d642;margin:0 0 6px 18px;position:relative}.universe-crew-tree>.universe-crew-button{grid-template-columns:18px minmax(0,1fr);width:calc(100% - 5px);min-height:46px;margin-left:5px;padding:8px 8px 8px 2px}.universe-crew-tree>.universe-crew-button.selected{background:linear-gradient(90deg,#46b18b1f,#359fb706);box-shadow:inset 2px 0 #76ebb7}.crew-branch-marker{color:#62b9cb94;font:9px var(--font-geist-mono), monospace;text-align:center;grid-row:1/3;align-self:start;line-height:14px}.universe-crew-tree .universe-crew-button strong{color:#b9d0d5}.universe-crew-tree .universe-crew-button.selected strong{color:#e0f2f4}.universe-system-facts{margin:10px 0 14px}.universe-system-facts>div{border-bottom:1px solid #699eaa1a;justify-content:space-between;gap:18px;padding:8px 0;font-size:9px;display:flex}.universe-system-facts dt{color:#60777e}.universe-system-facts dd{color:#a6babf;text-align:right;margin:0}.universe-station-select{margin-top:4px;display:block}.universe-station-select>span,.universe-search>label{color:#617a82;font:600 8px var(--font-geist-mono), monospace;letter-spacing:.08em;text-transform:uppercase;margin-bottom:7px;display:block}.universe-route-actions{grid-template-columns:minmax(0,.72fr) minmax(0,1.28fr);gap:7px;margin-top:12px;display:grid}.universe-route-guidance{color:#68838b;min-height:13px;font:9px/1.5 var(--font-geist-mono), monospace;margin:8px 0 0}.universe-route-guidance-blocked{color:#a98659}.universe-route-guidance-ready{color:#65a88d}.universe-route-error{border-left:1px solid var(--red);color:#c98989;margin:7px 0 0;padding-left:9px;font-size:9px;line-height:1.45}.universe-empty{color:#687f86;text-align:center;border:1px dashed #6ea1ad24;flex-direction:column;justify-content:center;min-height:86px;margin-top:12px;padding:14px;display:flex}.universe-empty strong,.universe-empty span{display:block}.universe-empty strong{color:#91a8ae;font-size:10px}.universe-empty span{margin-top:5px;font-size:8px;line-height:1.5}.universe-lower-grid{grid-template-columns:minmax(0,1.12fr) minmax(360px,.88fr);gap:8px;margin-top:8px;display:grid}.universe-route-panel,.universe-system-browser{min-height:250px}.universe-route-steps{scrollbar-color:#67a4b24d transparent;max-height:270px;margin:14px 0 0;padding:0;list-style:none;overflow:auto}.universe-route-steps li{border-bottom:1px solid #699eaa1a;grid-template-columns:28px minmax(0,1fr) auto;align-items:center;gap:9px;min-height:40px;display:grid}.universe-route-steps li>span{color:#4f6e77;font:8px var(--font-geist-mono), monospace}.universe-route-steps li>span:after{content:"";background:#72e2b238;width:1px;height:15px;margin:3px auto -14px;display:block}.universe-route-steps li:last-child>span:after{display:none}.universe-route-steps strong{color:#a9bec3;font-size:9px;font-weight:550}.universe-route-steps em{color:#76cfb0;font:normal 7px var(--font-geist-mono), monospace;text-transform:uppercase}.universe-search{margin-top:14px}.universe-search>div{grid-template-columns:minmax(0,1fr) auto;gap:7px;display:grid}.universe-search input{color:#c2d2d6;background:#0a1418;border:1px solid #6fa8b538;border-radius:0;outline:none;width:100%;height:36px;padding:0 11px}.universe-search input::placeholder{color:#50656b}.universe-search input:focus{border-color:#55d5ee8c}.universe-result-note{color:#5d737a;margin:9px 0;font-size:9px;line-height:1.45}.universe-system-list{scrollbar-color:#67a4b24d transparent;border-top:1px solid #699eaa1a;max-height:205px;overflow:auto}.universe-system-list button{grid-template-columns:9px minmax(0,1fr) auto;gap:9px;min-height:48px;padding:7px 8px}.universe-system-list button>i{border-radius:50%;width:5px;height:5px;box-shadow:0 0 8px}.universe-system-list button>em{color:#718990;font:normal 8px var(--font-geist-mono), monospace}.universe-load-state{min-height:430px}.feature-hero{grid-template-columns:minmax(320px,.78fr) minmax(360px,1.22fr);min-height:270px;margin-bottom:10px;display:grid}.feature-art{height:270px}.feature-message{align-self:center;padding:35px}.bracket-label{color:#65cde1;border-left:1px solid var(--blue);border-right:1px solid var(--blue);font:600 8px var(--font-geist-mono), monospace;letter-spacing:.12em;padding:5px 8px;display:inline-block}.feature-message h2{max-width:520px;margin-top:15px;font-size:24px}.feature-message p,.feature-card>p:not(.eyebrow){color:#7b9299;font-size:12px;line-height:1.45}.feature-message p{max-width:570px}.feature-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;display:grid}.feature-card{flex-direction:column;min-height:240px;padding:20px;display:flex}.feature-card .card-index{color:#344d55;font:500 27px var(--font-geist-mono), monospace;align-self:flex-end;margin-bottom:10px}.feature-card h2{font-size:16px}.feature-card .button{align-self:flex-start;margin-top:auto}.settings-layout{grid-template-columns:minmax(450px,1.4fr) minmax(300px,.6fr);gap:10px;display:grid}.toggle-panel,.connection-panel{padding:22px}.toggle-row{text-align:left;cursor:pointer;background:0 0;border:0;border-bottom:1px solid #6b9eaa1a;justify-content:space-between;align-items:center;gap:20px;width:100%;padding:15px 2px;display:flex}.toggle-row:hover{background:#55d5ee05}.toggle-row strong,.toggle-row small{display:block}.toggle-row strong{color:#b7c7ca;font-size:11px;font-weight:560}.toggle-row small{color:#62777e;margin-top:4px;font-size:9px}.settings-demo{margin:16px 0 4px}.module-status-row{border-bottom:1px solid #6b9eaa1a;justify-content:space-between;align-items:center;gap:20px;width:100%;min-height:66px;padding:12px 2px;display:flex}.module-status-row>span>strong,.module-status-row>span>small{display:block}.module-status-row>span>strong{color:#b7c7ca;font-size:11px;font-weight:560}.module-status-row>span>small{color:#62777e;margin-top:4px;font-size:9px}.toggle{background:#415b632e;border:1px solid #6b95a03d;flex:none;width:34px;height:16px;position:relative}.toggle b{background:#5a6f75;width:10px;height:10px;transition:left .14s,background .14s;position:absolute;top:2px;left:2px}.toggle.on{background:#3091a729;border-color:#55d5ee6b}.toggle.on b{background:var(--blue);left:20px;box-shadow:0 0 10px #55d5ee99}.connection-panel{text-align:center;flex-direction:column;align-items:center;display:flex}.connection-panel .eyebrow,.connection-panel h2{align-self:flex-start}.connection-orb{border:1px solid #55d5ee57;border-radius:50%;place-items:center;width:98px;height:98px;margin:28px 0 15px;display:grid;box-shadow:inset 0 0 34px #55d5ee14,0 0 30px #55d5ee0f}.connection-orb span{width:72px;height:72px;color:var(--blue-soft);font:520 21px var(--font-geist-mono), monospace;background:radial-gradient(circle,#378fa252,#112a313d);border-radius:50%;place-items:center;display:grid}.connection-panel>strong{color:#d1dddf;font-size:14px}.connection-panel>small{color:#668087;margin-top:4px;font-size:9px}.connection-panel dl{width:100%;margin:25px 0 18px}.connection-panel dl div{border-bottom:1px solid #6698a417;justify-content:space-between;gap:15px;padding:9px 0;font-size:9px;display:flex}.connection-panel dt{color:#627981}.connection-panel dd{color:#a8babf;margin:0}.connection-panel dd.good{color:var(--green)}.toast{right:20px;bottom:calc(16px + env(safe-area-inset-bottom));z-index:10;border:0;border-left:2px solid var(--blue);background:#081216f7;width:min(340px,100vw - 32px);padding:9px 11px;animation:.26s ease-out toast-in;position:fixed;box-shadow:0 8px 22px #0000004d}@keyframes toast-in{0%{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}.toast>div{grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:10px;display:grid}.toast-mark,.toast strong{display:none}.toast p{color:#a9bcc1;margin:0;font-size:10px;line-height:1.4}.toast .text-button{padding:4px;font-size:9px}.mobile-menu,.nav-scrim{display:none}@media (width<=1120px){.screen{padding-inline:24px}.metric-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid,.crew-grid{grid-template-columns:1fr}.industrial-command-band,.crew-package-facts{grid-template-columns:repeat(2,minmax(0,1fr))}.industrial-operations{grid-template-columns:minmax(220px,.38fr) minmax(0,1fr)}.site-row{grid-template-columns:32px minmax(160px,1fr) minmax(150px,.8fr)}.site-row .status-pill,.site-row .button{justify-self:stretch}.site-row .status-pill{justify-content:center}.site-row .button{grid-column:2/-1}.top-status>div:not(.pilot-chip){display:none}.feature-grid{grid-template-columns:1fr}.feature-card{min-height:190px}.universe-command-grid{grid-template-columns:1fr}.universe-command-rail{grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.universe-target-panel{min-height:280px}.universe-lower-grid{grid-template-columns:1fr 1fr}}@media (width<=980px){.crew-commission-topline{grid-template-columns:1fr;align-items:stretch}.industrial-operations{grid-template-columns:1fr}.system-browser,.site-command-panel{min-height:auto}.site-command-heading{grid-template-columns:1fr;align-items:stretch}.site-row{grid-template-columns:32px minmax(0,1fr) auto}.site-row .site-assignment{grid-column:2/-1}.site-row .status-pill{grid-area:1/3}.site-row .button{grid-column:2/-1}}@media (width<=760px){:root{--sidebar-width:230px}.app-shell{grid-template-columns:1fr}.sidebar{width:var(--sidebar-width);transition:transform .18s;position:fixed;top:0;bottom:0;left:0;transform:translate(-102%)}.sidebar.open{transform:translate(0)}.workspace{height:100dvh}.topbar{padding:0 14px}.mobile-menu{color:#8ad9e8;font:600 8px var(--font-geist-mono), monospace;background:#368b9f1a;border:1px solid #55d5ee4d;padding:6px 8px;display:block}.breadcrumb span{display:none}.top-status .pilot-chip{min-width:auto;padding:0 0 0 12px}.pilot-chip>span:last-child{display:none}.screen{padding:25px 14px 82px}.module-header{flex-direction:column;align-items:flex-start}.header-actions{width:100%}.header-actions .button{flex:1}.metric-grid{grid-template-columns:1fr 1fr}.metric{min-height:94px;padding:13px}.metric-value{font-size:20px}.wing-visual{grid-template-columns:1fr}.link-line{height:35px}.link-line:before,.link-line:after{width:44%}.operation-readout,.crew-stats{grid-template-columns:1fr}.command-band{grid-template-columns:1fr;align-items:stretch}.crew-package-facts,.crew-pricing-note,.industrial-operations{grid-template-columns:1fr}.system-browser,.site-command-panel{min-height:auto}.site-command-heading{grid-template-columns:1fr;align-items:stretch}.site-row{grid-template-columns:32px minmax(0,1fr) auto}.site-row .site-assignment{grid-column:2/-1}.site-row .status-pill{grid-area:1/3}.site-row .button{grid-column:2/-1}.crew-card{grid-template-columns:1fr}.crew-card .ship-portrait{height:210px;min-height:210px}.ledger-grid,.feature-hero{grid-template-columns:1fr}.feature-art{height:210px}.feature-message{padding:23px}.settings-layout,.universe-command-rail,.universe-lower-grid{grid-template-columns:1fr}.universe-map-stage{height:470px;min-height:470px}.universe-live-disclosure{flex-direction:column;align-items:flex-start}.universe-map-legend{right:12px}.nav-scrim{z-index:4;background:#0000008f;border:0;display:block;position:fixed;inset:0}}@media (width<=450px){.metric-grid{grid-template-columns:1fr}.demo-disclosure{flex-direction:column;align-items:flex-start}.action-constraint{grid-template-columns:1fr}.module-status-row{flex-direction:column;align-items:flex-start}.site-row{grid-template-columns:1fr}.site-row .site-marker,.site-row .site-assignment,.site-row .status-pill,.site-row .button{grid-area:auto/1}.site-row .status-pill{justify-self:start}.button-row{flex-wrap:wrap}.button-row .button{flex:auto}.universe-map-panel{padding:0}.universe-map-stage{height:390px;min-height:390px}.universe-map-heading{flex-direction:column;align-items:flex-start}.universe-map-legend{display:none}.universe-route-actions,.universe-search>div{grid-template-columns:1fr}.universe-search .button{width:100%}.toast{width:calc(100vw - 20px);bottom:10px;right:10px}}@media (pointer:coarse){.nav-group button,.universe-operation-node>.universe-operation-button,.universe-crew-tree>.universe-crew-button{min-height:44px}.universe-map-controls{grid-template-columns:44px 44px 52px}.universe-map-controls button{height:44px}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important}} diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/assets/index-CG2pp9Po.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/assets/index-CG2pp9Po.js new file mode 100644 index 0000000..6d13498 --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/assets/index-CG2pp9Po.js @@ -0,0 +1,9 @@ +var e=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports);(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var t=e((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},ee=Object.prototype.hasOwnProperty;function te(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ne(e,t){return te(e.type,t,e.props)}function T(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function E(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var D=/\/+/g;function re(e,t){return typeof e==`object`&&e&&e.key!=null?E(``+e.key):t.toString(36)}function O(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function ie(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,ie(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+re(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(D,`$&/`)+`/`),ie(o,r,i,``,function(e){return e})):o!=null&&(T(o)&&(o=ne(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(D,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{n.exports=t()})),r=e((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,T());else{var t=n(l);t!==null&&re(x,t.startTime-e)}}var S=!1,C=-1,w=5,ee=-1;function te(){return g?!0:!(e.unstable_now()-eet&&te());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&re(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}}}finally{i?T():S=!1}}}var T;if(typeof y==`function`)T=function(){y(ne)};else if(typeof MessageChannel<`u`){var E=new MessageChannel,D=E.port2;E.port1.onmessage=ne,T=function(){D.postMessage(null)}}else T=function(){_(ne,0)};function re(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,re(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,T()))),r},e.unstable_shouldYield=te,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),i=e(((e,t)=>{t.exports=r()})),a=e((e=>{var t=n();function r(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=a()})),s=e((e=>{var t=i(),r=n(),a=o();function s(e){var t=`https://react.dev/errors/`+e;if(1se||(e.current=oe[se],oe[se]=null,se--)}function P(e,t){se++,oe[se]=e.current,e.current=t}var le=ce(null),ue=ce(null),de=ce(null),fe=ce(null);function pe(e,t){switch(P(de,t),P(ue,e),P(le,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}N(le),P(le,e)}function me(){N(le),N(ue),N(de)}function he(e){e.memoizedState!==null&&P(fe,e);var t=le.current,n=Hd(t,e.type);t!==n&&(P(ue,e),P(le,n))}function ge(e){ue.current===e&&(N(le),N(ue)),fe.current===e&&(N(fe),Qf._currentValue=ae)}var _e,ve;function ye(e){if(_e===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);_e=t&&t[1]||``,ve=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{be=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ye(n):``}function Se(e,t){switch(e.tag){case 26:case 27:case 5:return ye(e.type);case 16:return ye(`Lazy`);case 13:return e.child!==t&&t!==null?ye(`Suspense Fallback`):ye(`Suspense`);case 19:return ye(`SuspenseList`);case 0:case 15:return xe(e.type,!1);case 11:return xe(e.type.render,!1);case 1:return xe(e.type,!0);case 31:return ye(`Activity`);default:return``}}function Ce(e){try{var t=``,n=null;do t+=Se(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var we=Object.prototype.hasOwnProperty,Te=t.unstable_scheduleCallback,Ee=t.unstable_cancelCallback,De=t.unstable_shouldYield,Oe=t.unstable_requestPaint,F=t.unstable_now,ke=t.unstable_getCurrentPriorityLevel,Ae=t.unstable_ImmediatePriority,je=t.unstable_UserBlockingPriority,Me=t.unstable_NormalPriority,Ne=t.unstable_LowPriority,Pe=t.unstable_IdlePriority,Fe=t.log,Ie=t.unstable_setDisableYieldValue,Le=null,Re=null;function ze(e){if(typeof Fe==`function`&&Ie(e),Re&&typeof Re.setStrictMode==`function`)try{Re.setStrictMode(Le,e)}catch{}}var Be=Math.clz32?Math.clz32:Ue,Ve=Math.log,He=Math.LN2;function Ue(e){return e>>>=0,e===0?32:31-(Ve(e)/He|0)|0}var We=256,Ge=262144,Ke=4194304;function qe(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Je(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=qe(n))):i=qe(o):i=qe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=qe(n))):i=qe(o)):i=qe(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ye(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Xe(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Ze(){var e=Ke;return Ke<<=1,!(Ke&62914560)&&(Ke=4194304),e}function Qe(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function $e(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function et(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),pn=!1;if(fn)try{var mn={};Object.defineProperty(mn,"passive",{get:function(){pn=!0}}),window.addEventListener(`test`,mn,mn),window.removeEventListener(`test`,mn,mn)}catch{pn=!1}var hn=null,gn=null,_n=null;function vn(){if(_n)return _n;var e,t=gn,n=t.length,r,i=`value`in hn?hn.value:hn.textContent,a=i.length;for(e=0;e=Xn),$n=` `,er=!1;function tr(e,t){switch(e){case`keyup`:return Jn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function nr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var rr=!1;function ir(e,t){switch(e){case`compositionend`:return nr(t);case`keypress`:return t.which===32?(er=!0,$n):null;case`textInput`:return e=t.data,e===$n&&er?null:e;default:return null}}function ar(e,t){if(rr)return e===`compositionend`||!Yn&&tr(e,t)?(e=vn(),_n=gn=hn=null,rr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Dr(n)}}function kr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?kr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ar(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=zt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=zt(e.document)}return t}function jr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Mr=fn&&`documentMode`in document&&11>=document.documentMode,Nr=null,Pr=null,Fr=null,Ir=!1;function Lr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ir||Nr==null||Nr!==zt(r)||(r=Nr,`selectionStart`in r&&jr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Fr&&Er(Fr,r)||(Fr=r,r=Ed(Pr,`onSelect`),0>=o,i-=o,ki=1<<32-Be(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),o=a(_,o,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),L&&ji(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(i,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(i,h),o=a(y,o,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(i,h),L&&ji(i,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(i,v.value,l),v!==null&&(o=a(v,o,g),d===null?u=v:d.sibling=v,d=v);return L&&ji(i,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,i,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),o=a(v,o,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(i,e)}),L&&ji(i,g),u}function b(e,r,a,c){if(typeof a==`object`&&a&&a.type===y&&a.key===null&&(a=a.props.children),typeof a==`object`&&a){switch(a.$$typeof){case _:a:{for(var l=a.key;r!==null;){if(r.key===l){if(l=a.type,l===y){if(r.tag===7){n(e,r.sibling),c=i(r,a.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===T&&Aa(l)===r.type){n(e,r.sibling),c=i(r,a.props),La(c,a),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}a.type===y?(c=gi(a.props.children,e.mode,c,a.key),c.return=e,e=c):(c=hi(a.type,a.key,a.props,null,e.mode,c),La(c,a),c.return=e,e=c)}return o(e);case v:a:{for(l=a.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===a.containerInfo&&r.stateNode.implementation===a.implementation){n(e,r.sibling),c=i(r,a.children||[]),c.return=e,e=c;break a}else{n(e,r);break}t(e,r),r=r.sibling}c=yi(a,e.mode,c),c.return=e,e=c}return o(e);case T:return a=Aa(a),b(e,r,a,c)}if(A(a))return h(e,r,a,c);if(O(a)){if(l=O(a),typeof l!=`function`)throw Error(s(150));return a=l.call(a),g(e,r,a,c)}if(typeof a.then==`function`)return b(e,r,Ia(a),c);if(a.$$typeof===C)return b(e,r,ia(e,a),c);Ra(e,a)}return typeof a==`string`&&a!==``||typeof a==`number`||typeof a==`bigint`?(a=``+a,r!==null&&r.tag===6?(n(e,r.sibling),c=i(r,a),c.return=e,e=c):(n(e,r),c=_i(a,e.mode,c),c.return=e,e=c),o(e)):n(e,r)}return function(e,t,n,r){try{Fa=0;var i=b(e,t,n,r);return Pa=null,i}catch(t){if(t===wa||t===Ea)throw t;var a=di(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ba=za(!0),Va=za(!1),Ha=!1;function Ua(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Wa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ga(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ka(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,G&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ci(e),si(e,null,n),t}return ii(e,r,t,n),ci(e)}function qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,nt(e,n)}}function Ja(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ya=!1;function Xa(){if(Ya){var e=ha;if(e!==null)throw e}}function Za(e,t,n,r){Ya=!1;var i=e.updateQueue;Ha=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(J&f)===f:(r&f)===f){f!==0&&f===ma&&(Ya=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Ha=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function Qa(e,t){if(typeof e!=`function`)throw Error(s(191,e));e.call(t)}function $a(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=j.T,s={};j.T=s,Fs(e,!1,t,n);try{var c=i(),l=j.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ps(e,t,va(c,r),pu(e)):Ps(e,t,r,pu(e))}catch(n){Ps(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{M.p=a,o!==null&&s.types!==null&&(o.types=s.types),j.T=o}}function ws(){}function Ts(e,t,n,r){if(e.tag!==5)throw Error(s(476));var i=Es(e).queue;Cs(e,i,t,ae,n===null?ws:function(){return Ds(e),n(r)})}function Es(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ae,baseState:ae,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:ae},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ds(e){var t=Es(e);t.next===null&&(t=e.alternate.memoizedState),Ps(e,t.next.queue,{},pu())}function Os(){return ra(Qf)}function ks(){return H().memoizedState}function As(){return H().memoizedState}function js(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ga(n);var r=Ka(t,e,n);r!==null&&(hu(r,t,n),qa(r,t,n)),t={cache:ua()},e.payload=t;return}t=t.return}}function Ms(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Is(e)?Ls(t,n):(n=ai(e,t,n,r),n!==null&&(hu(n,e,r),Rs(n,t,r)))}function Ns(e,t,n){Ps(e,t,n,pu())}function Ps(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Is(e))Ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Tr(s,o))return ii(e,t,i,0),K===null&&ri(),!1}catch{}if(n=ai(e,t,i,r),n!==null)return hu(n,e,r),Rs(n,t,r),!0}return!1}function Fs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Is(e)){if(t)throw Error(s(479))}else t=ai(e,n,r,2),t!==null&&hu(t,e,2)}function Is(e){var t=e.alternate;return e===z||t!==null&&t===z}function Ls(e,t){_o=go=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Rs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,nt(e,n)}}var zs={readContext:ra,use:Po,useCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useLayoutEffect:V,useInsertionEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V,useDeferredValue:V,useTransition:V,useSyncExternalStore:V,useId:V,useHostTransitionStatus:V,useFormState:V,useActionState:V,useOptimistic:V,useMemoCache:V,useCacheRefresh:V};zs.useEffectEvent=V;var Bs={readContext:ra,use:Po,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:ra,useEffect:us,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),cs(4194308,4,gs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return cs(4194308,4,e,t)},useInsertionEffect:function(e,t){cs(4,2,e,t)},useMemo:function(e,t){var n=jo();t=t===void 0?null:t;var r=e();if(vo){ze(!0);try{e()}finally{ze(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=jo();if(n!==void 0){var i=n(t);if(vo){ze(!0);try{n(t)}finally{ze(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ms.bind(null,z,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:function(e){e=Ko(e);var t=e.queue,n=Ns.bind(null,z,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:vs,useDeferredValue:function(e,t){return xs(jo(),e,t)},useTransition:function(){var e=Ko(!1);return e=Cs.bind(null,z,e.queue,!0,!1),jo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=z,i=jo();if(L){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),K===null)throw Error(s(349));J&127||Vo(r,t,n)}i.memoizedState=n;var a={value:n,getSnapshot:t};return i.queue=a,us(Uo.bind(null,r,a,e),[e]),r.flags|=2048,os(9,{destroy:void 0},Ho.bind(null,r,a,n,t),null),n},useId:function(){var e=jo(),t=K.identifierPrefix;if(L){var n=Ai,r=ki;n=(r&~(1<<32-Be(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=yo++,0<\/script>`,a=a.removeChild(a.firstChild);break;case`select`:a=typeof r.is==`string`?o.createElement(`select`,{is:r.is}):o.createElement(`select`),r.multiple?a.multiple=!0:r.size&&(a.size=r.size);break;default:a=typeof r.is==`string`?o.createElement(i,{is:r.is}):o.createElement(i)}}a[lt]=t,a[ut]=r;a:for(o=t.child;o!==null;){if(o.tag===5||o.tag===6)a.appendChild(o.stateNode);else if(o.tag!==4&&o.tag!==27&&o.child!==null){o.child.return=o,o=o.child;continue}if(o===t)break a;for(;o.sibling===null;){if(o.return===null||o.return===t)break a;o=o.return}o.sibling.return=o.return,o=o.sibling}t.stateNode=a;a:switch(Pd(a,i,r),i){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Pc(t)}}return U(t),Fc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Pc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(s(166));if(e=de.current,Ui(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,i=Ii,i!==null)switch(i.tag){case 27:case 5:r=i.memoizedProps}e[lt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Bi(t,!0)}else e=Bd(e).createTextNode(r),e[lt]=t,t.stateNode=e}return U(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ui(t),n!==null){if(e===null){if(!r)throw Error(s(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(s(557));e[lt]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),e=!1}else n=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(fo(t),t):(fo(t),null);if(t.flags&128)throw Error(s(558))}return U(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(i=Ui(t),r!==null&&r.dehydrated!==null){if(e===null){if(!i)throw Error(s(318));if(i=t.memoizedState,i=i===null?null:i.dehydrated,!i)throw Error(s(317));i[lt]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),i=!1}else i=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=i),i=!0;if(!i)return t.flags&256?(fo(t),t):(fo(t),null)}return fo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,i=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(i=r.alternate.memoizedState.cachePool.pool),a=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(a=r.memoizedState.cachePool.pool),a!==i&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Lc(t,t.updateQueue),U(t),null);case 4:return me(),e===null&&Sd(t.stateNode.containerInfo),U(t),null;case 10:return Zi(t.type),U(t),null;case 19:if(N(R),r=t.memoizedState,r===null)return U(t),null;if(i=!!(t.flags&128),a=r.rendering,a===null)if(i)Rc(r,!1);else{if(X!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(a=po(e),a!==null){for(t.flags|=128,Rc(r,!1),e=a.updateQueue,t.updateQueue=e,Lc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)mi(n,e),n=n.sibling;return P(R,R.current&1|2),L&&ji(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&F()>tu&&(t.flags|=128,i=!0,Rc(r,!1),t.lanes=4194304)}else{if(!i)if(e=po(a),e!==null){if(t.flags|=128,i=!0,e=e.updateQueue,t.updateQueue=e,Lc(t,e),Rc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!a.alternate&&!L)return U(t),null}else 2*F()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,i=!0,Rc(r,!1),t.lanes=4194304);r.isBackwards?(a.sibling=t.child,t.child=a):(e=r.last,e===null?t.child=a:e.sibling=a,r.last=a)}return r.tail===null?(U(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=F(),e.sibling=null,n=R.current,P(R,i?n&1|2:n&1),L&&ji(t,r.treeForkCount),e);case 22:case 23:return fo(t),io(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(U(t),t.subtreeFlags&6&&(t.flags|=8192)):U(t),n=t.updateQueue,n!==null&&Lc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&N(ba),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Zi(la),U(t),null;case 25:return null;case 30:return null}throw Error(s(156,t.tag))}function Bc(e,t){switch(Pi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Zi(la),me(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return ge(t),null;case 31:if(t.memoizedState!==null){if(fo(t),t.alternate===null)throw Error(s(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(fo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(s(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return N(R),null;case 4:return me(),null;case 10:return Zi(t.type),null;case 22:case 23:return fo(t),io(),e!==null&&N(ba),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Zi(la),null;case 25:return null;default:return null}}function Vc(e,t){switch(Pi(t),t.tag){case 3:Zi(la),me();break;case 26:case 27:case 5:ge(t);break;case 4:me();break;case 31:t.memoizedState!==null&&fo(t);break;case 13:fo(t);break;case 19:N(R);break;case 10:Zi(t.type);break;case 22:case 23:fo(t),io(),e!==null&&N(ba);break;case 24:Zi(la)}}function Hc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Uc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Wc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{$a(t,n)}catch(t){Z(e,e.return,t)}}}function Gc(e,t,n){n.props=qs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Kc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function qc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Jc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Yc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[ut]=t}catch(t){Z(e,e.return,t)}}function Xc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Zc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Xc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=nn));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[lt]=e,t[ut]=n}catch(t){Z(e,e.return,t)}}var tl=!1,nl=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Rd=sp,e=Ar(e),jr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var i=r.anchorOffset,a=r.focusNode;r=r.focusOffset;try{n.nodeType,a.nodeType}catch{n=null;break a}var o=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||i!==0&&f.nodeType!==3||(c=o+i),f!==a||r!==0&&f.nodeType!==3||(l=o+r),f.nodeType===3&&(o+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===i&&(c=o),p===a&&++d===r&&(l=o),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,a=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(a,r,n),a[lt]=e,St(a),r=a;break a;case`link`:var o=Vf(`link`,`href`,i).get(r+(n.href||``));if(o){for(var c=0;cg&&(o=g,g=h,h=o);var _=Or(s,h),v=Or(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,j.T=null,n=lu,lu=null;var a=au,o=su;if(iu=0,ou=au=null,su=0,G&6)throw Error(s(331));var c=G;if(G|=4,Fl(a.current),Dl(a,a.current,o,n),G=c,id(0,!1),Re&&typeof Re.onPostCommitFiberRoot==`function`)try{Re.onPostCommitFiberRoot(Le,a)}catch{}return!0}finally{M.p=i,j.T=r,Vu(e,t)}}function Wu(e,t,n){t=xi(n,t),t=$s(e.stateNode,t,2),e=Ka(e,t,2),e!==null&&($e(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=xi(n,e),n=ec(2),r=Ka(t,n,2),r!==null&&(tc(n,r,t,e),$e(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,K===e&&(J&n)===n&&(X===4||X===3&&(J&62914560)===J&&300>F()-$l?!(G&2)&&Su(e,0):ql|=n,Yl===J&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=Ze()),e=oi(e,t),e!==null&&($e(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,i=e.memoizedState;i!==null&&(n=i.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(s(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return Te(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Be(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=J,a=Je(r,r===K?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ye(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=F(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Vt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),St(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Vt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Vt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Vt(n.imageSizes)+`"]`)):i+=`[href="`+Vt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),St(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Vt(r)+`"][href="`+Vt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),St(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=xt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);St(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=xt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),St(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=xt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),St(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var i=(i=de.current)?gf(i):null;if(!i)throw Error(s(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=xt(i).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var a=xt(i).hoistableStyles,o=a.get(e);if(o||(i=i.ownerDocument||i,o={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},a.set(e,o),(a=i.querySelector(jf(e)))&&!a._p&&(o.instance=a,o.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),a||Nf(i,e,n,o.state))),t&&r===null)throw Error(s(528,``));return o}if(t&&r!==null)throw Error(s(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=xt(i).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(s(444,e))}}function Af(e){return`href="`+Vt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),St(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Vt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Vt(n.href)+`"]`);if(r)return t.instance=r,St(r),r;var i=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),St(r),Pd(r,`style`,i),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:i=Af(n.href);var a=e.querySelector(jf(i));if(a)return t.state.loading|=4,t.instance=a,St(a),a;r=Mf(n),(i=mf.get(i))&&Rf(r,i),a=(e.ownerDocument||e).createElement(`link`),St(a);var o=a;return o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),t.state.loading|=4,Lf(a,n.precedence,e),t.instance=a;case`script`:return a=Pf(n.src),(i=e.querySelector(Ff(a)))?(t.instance=i,St(i),i):(r=n,(i=mf.get(a))&&(r=h({},n),zf(r,i)),e=e.ownerDocument||e,i=e.createElement(`script`),St(i),Pd(i,`link`,r),e.head.appendChild(i),t.instance=i);case`void`:return null;default:throw Error(s(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,St(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),St(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=s()})),l=n(),u=c(),d=class extends Error{};function f(e,t){return e===t}function p(e,t,n,r,i){return(e&&t?{apiBase:e,token:t}:null)||n||(r&&i?{apiBase:r,token:i}:null)||null}function m(e,t={}){let n={accept:`application/json`,...t};return e&&(n.authorization=`Bearer ${e}`),n}async function h(e,t,n=fetch){let r=await n(`${e.replace(/\/$/,``)}/api/v1/auth/exchange`,{method:`POST`,headers:m(``,{"content-type":`application/json`}),body:JSON.stringify({token:t}),credentials:`include`,cache:`no-store`}),i=await r.json().catch(()=>({}));if(r.status===401)throw new d(i.error?.message||`This character link has expired.`);if(!r.ok||i.success!==!0)throw Error(i.error?.message||`The EveJS bridge could not establish a browser session.`);return i.data||{}}async function g(e,t,n=fetch){let r=await n(`${e.replace(/\/$/,``)}/api/v1/session`,{headers:m(t),credentials:`include`,cache:`no-store`}),i=await r.json().catch(()=>({}));if(r.status===401)throw new d(i.error?.message||`This character link has expired.`);if(!r.ok||i.success!==!0||!i.data)throw Error(i.error?.message||`The EveJS bridge rejected this character link.`);return i.data}function _(e=h,t=g){let n=null;return{load(r,i){if(n?.apiBase===r)return n.promise;let a=i?e(r,i).then(()=>t(r,``)):t(r,``);return n={apiBase:r,promise:a},a},clear(){n=null}}}function v(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:null}function y(e){return Array.isArray(e)?e:[]}function b(...e){for(let t of e){if(t==null||t===``)continue;let e=Number(t);if(Number.isFinite(e))return e}return null}function x(...e){for(let t of e){if(t==null||t===``)continue;let e=Number(t);if(Number.isFinite(e)&&e>0)return e}return null}function S(...e){for(let t of e){if(typeof t==`string`&&t.trim())return t.trim();if(typeof t==`number`&&Number.isFinite(t))return String(t)}return``}function C(e){let t=e;for(let e=0;e<3;e+=1){let e=v(t);if(!e)break;if(e.success===!1)throw Error(S(e.error?.message,e.message,`EveJS rejected the Universe request.`));if(!v(e.data))break;t=e.data}return v(t)||{}}function w(e){return y(e).map(e=>{let t=v(e);return b(t?.systemID,t?.solarSystemID,t?.id,e)}).filter(e=>e!==null)}function ee(e){let t=C(e),n=v(t.topology)||t,r=[...y(n.systems),...y(n.solarSystems),...y(n.nodes)],i=new Map;for(let e of r){let t=v(e);if(!t)continue;let n=v(t.position)||v(t.coordinates)||{},r=b(t.systemID,t.solarSystemID,t.id),a=b(t.x,n.x),o=b(t.z,n.z,t.mapY,t.y,n.y);r!==null&&a!==null&&o!==null&&i.set(r,{systemID:r,systemName:S(t.systemName,t.solarSystemName,t.name,`System ${r}`),x:a,z:o,securityStatus:b(t.securityStatus,t.security,t.securityLevel),regionID:b(t.regionID,t.region?.regionID,t.region?.id),regionName:S(t.regionName,t.region?.regionName,t.region?.name)})}let a=[...y(n.edges),...y(n.connections),...y(n.stargates),...y(n.links)],o=new Map;for(let e of a){let t=v(e);if(!t)continue;let n=v(t.destination)||{},r=b(t.fromSystemID,t.fromSolarSystemID,t.sourceSystemID,t.solarSystemID,t.from,t.a,v(t.source)?.systemID,v(t.source)?.solarSystemID),a=b(t.toSystemID,t.toSolarSystemID,t.destinationSolarSystemID,t.destinationSystemID,t.to,t.b,t.target,n.systemID,n.solarSystemID);if(r===null||a===null||r===a||!i.has(r)||!i.has(a))continue;let s=Math.min(r,a),c=Math.max(r,a),l=`${s}:${c}`;o.set(l,{fromSystemID:s,toSystemID:c,gateID:b(t.gateID,t.stargateID,t.fromStargateID,t.itemID,t.id)})}let s=[...y(n.stations),...y(n.destinations)],c=new Map;for(let e of s){let t=v(e);if(!t)continue;let n=b(t.stationID,t.itemID,t.locationID,t.id),r=b(t.systemID,t.solarSystemID,t.location?.systemID,t.location?.solarSystemID);n===null||r===null||!i.has(r)||c.set(n,{stationID:n,stationName:S(t.stationName,t.itemName,t.name,`Station ${n}`),systemID:r})}let l=[...i.values()],u=[...o.values()],d=[...c.values()];if(!l.length)throw Error(`The EveJS topology response did not contain any drawable solar systems.`);return{version:S(n.version,n.topologyVersion,n.generatedAtMs,`unversioned`),systems:l,edges:u,stations:d}}function te(e){let t=C(e),n=v(t.snapshot)||t,r=v(n.character)||{},i=v(n.industrial)||{},a=(y(n.assets).length?y(n.assets):y(n.contracts).length?y(n.contracts):y(i.contracts)).map((e,t)=>{let n=v(e)||{},r=v(n.location)||{},i=v(n.navigation)||{},a=v(n.assignment)||{},o=S(n.contractID,n.contractId),s=S(n.assetID,n.shipID,o,`asset-${t+1}`),c=S(n.role,n.assetType,`operation`),l=S(n.crewName,n.displayName,n.contractName,n.pilotName,n.name,`${c.replace(/[_-]+/g,` `)} crew ${t+1}`);return{assetID:s,contractID:o||null,name:l,pilotName:S(n.pilotName),role:c,contractKind:S(n.contractKind),hullCount:b(n.hullCount),state:S(n.phase,n.state,n.order,i.phase,`unknown`),systemID:x(n.systemID,n.solarSystemID,n.currentSystemID,r.systemID,r.solarSystemID,i.currentSystemID,i.systemID),assignedSystemID:x(n.assignedSystemID,a.systemID,a.solarSystemID,n.site?.systemID),assignedSystemName:S(n.assignedSystemName,a.systemName,n.site?.systemName),assignedSiteID:x(n.assignedSiteID,a.siteID,a.assignedSiteID,n.site?.siteID,n.site?.id),assignedSiteName:S(n.assignedSiteName,a.siteName,a.assignedSiteName,n.site?.siteName,n.site?.name),homeStationID:x(n.homeStationID,i.homeStationID),destinationStationID:x(n.destinationStationID,i.destinationStationID),destinationSystemID:x(n.destinationSystemID,i.destinationSystemID),routeSystemIDs:w(n.routeSystemIDs||n.route||i.systemIDs||i.routeSystemIDs),routeIndex:b(n.routeIndex,i.routeIndex,i.legIndex),etaAtMs:b(n.etaAtMs,i.etaAtMs,i.arrivalAtMs)}});return{generatedAtMs:b(n.generatedAtMs,n.capturedAtMs,n.updatedAtMs),characterSystemID:x(n.characterSystemID,n.solarSystemID,r.solarSystemID,r.systemID),assets:a}}function ne(e){let t=y(e),n=new Map;for(let e of t){let t=v(e);if(!t)continue;let r=x(t.assignedSiteID),i=x(t.assignedSystemID),a=x(t.systemID),o=x(t.destinationSystemID),s=o!==null&&a!==null&&a!==o,c=r===null?i===null?s?`transit:${o}`:a===null?`unassigned`:`system:${a}`:`system:${i}`:`site:${r}`,l=r===null?s?`transit`:c===`unassigned`?`unassigned`:`system`:`site`,u=a??i,d=S(t.assignedSystemName,u===null?``:`System ${u}`),f=r===null?s?`Transit to System ${o}`:u===null?`Unassigned crews`:`${d} operation`:S(t.assignedSiteName,`Industrial site ${r}`),p=n.get(c);if(p){p.crews.push(t),p.systemID===null&&u!==null&&(p.systemID=u),!p.systemName&&d&&(p.systemName=d);continue}n.set(c,{operationID:c,operationKind:l,name:f,systemID:u,systemName:d,destinationSystemID:s?o:null,crews:[t]})}return[...n.values()].map(e=>({...e,crews:[...e.crews].sort((e,t)=>S(e.name).localeCompare(S(t.name)))})).sort((e,t)=>S(e.systemName).localeCompare(S(t.systemName))||e.name.localeCompare(t.name))}function T(e){let t=C(e),n=v(t.route)||t,r=w(n.systemIDs||n.path||n.systems||n.route);if(!r.length)throw Error(`EveJS returned an empty route.`);return{systemIDs:r,jumps:b(n.jumps,n.jumpCount)??Math.max(0,r.length-1),fromSystemID:b(n.fromSystemID,n.from,r[0]),toSystemID:b(n.toSystemID,n.to,r[r.length-1])}}var E=e((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),D=e(((e,t)=>{t.exports=E()}))(),re=.35,O=2048,ie=64,k=4,A=1.8,j={version:`demo-1`,systems:[{systemID:99000001,systemName:`Demo Nexus`,x:0,z:20,securityStatus:.9,regionID:9901,regionName:`Demo Region`},{systemID:99000002,systemName:`Demo Foundry`,x:118,z:55,securityStatus:.8,regionID:9901,regionName:`Demo Region`},{systemID:99000003,systemName:`Demo Crossing`,x:228,z:18,securityStatus:.6,regionID:9901,regionName:`Demo Region`},{systemID:99000004,systemName:`Demo Frontier`,x:346,z:82,securityStatus:.4,regionID:9901,regionName:`Demo Region`},{systemID:99000005,systemName:`Demo Reach`,x:458,z:36,securityStatus:.2,regionID:9901,regionName:`Demo Region`},{systemID:99000006,systemName:`Demo Expanse`,x:566,z:110,securityStatus:-.1,regionID:9902,regionName:`Demo Frontier`},{systemID:99000007,systemName:`Demo Haven`,x:152,z:-72,securityStatus:.7,regionID:9901,regionName:`Demo Region`},{systemID:99000008,systemName:`Demo Anchorage`,x:314,z:-76,securityStatus:.5,regionID:9901,regionName:`Demo Region`},{systemID:99000009,systemName:`Demo Verge`,x:476,z:-68,securityStatus:.1,regionID:9902,regionName:`Demo Frontier`}],edges:[[99000001,99000002],[99000002,99000003],[99000003,99000004],[99000004,99000005],[99000005,99000006],[99000002,99000007],[99000007,99000008],[99000008,99000009],[99000009,99000006],[99000003,99000008],[99000004,99000008]].map(([e,t])=>({fromSystemID:e,toSystemID:t,gateID:null})),stations:[{stationID:99100001,stationName:`Demo Nexus Command Station`,systemID:99000001},{stationID:99100004,stationName:`Demo Frontier Trade Post`,systemID:99000004},{stationID:99100006,stationName:`Demo Expanse Anchorage`,systemID:99000006},{stationID:99100008,stationName:`Demo Anchorage Depot`,systemID:99000008}]},M={generatedAtMs:null,characterSystemID:99000001,assets:[{assetID:`demo-miner`,contractID:`demo-miner`,name:`Demo Venture Crew`,pilotName:`Sample Crew Alpha`,role:`miner`,contractKind:`mining_crew`,hullCount:3,state:`standby`,systemID:99000002,assignedSystemID:99000002,assignedSystemName:`Demo Foundry`,assignedSiteID:99200002,assignedSiteName:`Demo Foundry Belt Operation`,homeStationID:99100001,destinationStationID:null,destinationSystemID:null,routeSystemIDs:[],routeIndex:null,etaAtMs:null},{assetID:`demo-hauler`,contractID:`demo-hauler`,name:`Demo Hauling Crew`,pilotName:`Sample Crew Beta`,role:`hauler`,contractKind:`industrial`,hullCount:1,state:`working`,systemID:99000007,assignedSystemID:99000007,assignedSystemName:`Demo Haven`,assignedSiteID:null,assignedSiteName:``,homeStationID:99100001,destinationStationID:null,destinationSystemID:null,routeSystemIDs:[],routeIndex:null,etaAtMs:null}]};function ae(e,t=1){return e===null?`rgba(126, 148, 155, ${t})`:e>=.5?`rgba(87, 215, 239, ${t})`:e>0?`rgba(229, 178, 87, ${t})`:`rgba(224, 105, 105, ${t})`}function oe(e,t=`Unknown`){return e?e.replace(/[_-]+/g,` `).replace(/\b\w/g,e=>e.toUpperCase()):t}function se(e){if(!e.length)return{points:new Map,width:1,height:1};let t=Math.min(...e.map(e=>e.x)),n=Math.max(...e.map(e=>e.x)),r=Math.min(...e.map(e=>e.z)),i=Math.max(...e.map(e=>e.z)),a=Math.max(1,n-t),o=Math.max(1,i-r),s=Math.max(a,o),c=a/s*1e3,l=o/s*1e3,u=new Map;for(let n of e)u.set(n.systemID,{systemID:n.systemID,x:(n.x-t)/a*c,y:(1-(n.z-r)/o)*l});return{points:u,width:c,height:l}}function ce(e,t){let n=Math.max(1,t.width-116),r=Math.max(1,t.height-116),i=Math.max(.01,Math.min(n/e.width,r/e.height));return{scale:i,x:(t.width-e.width*i)/2,y:(t.height-e.height*i)/2}}function N(e,t,n){if(t===n)return{systemIDs:[t],jumps:0,fromSystemID:t,toSystemID:n};let r=new Map;for(let t of e.edges)r.set(t.fromSystemID,[...r.get(t.fromSystemID)||[],t.toSystemID]),r.set(t.toSystemID,[...r.get(t.toSystemID)||[],t.fromSystemID]);let i=[t],a=new Map([[t,null]]);for(let e=0;e({}));if(!a.ok)throw Error(o.error?.message||o.message||`EveJS returned ${a.status} for the Universe request.`);return o}function le(e,t,n){return e?e.systemID??e.assignedSystemID??(e.homeStationID===null?null:n.get(e.homeStationID)?.systemID??null)??t.characterSystemID:t.characterSystemID}function ue({topology:e,snapshot:t,operations:n,route:r,selectedSystemID:i,selectedAssetID:a,selectedOperationID:o,focusRequest:s,demo:c,onSelectSystem:u}){let d=(0,l.useRef)(null),f=(0,l.useRef)({active:!1,pointerID:-1,x:0,y:0,moved:!1}),p=(0,l.useRef)({at:0,x:0,y:0}),[m,h]=(0,l.useState)({width:1,height:1,dpr:1}),g=(0,l.useMemo)(()=>{let t=new Set;for(let n of e.edges)t.add(n.fromSystemID),t.add(n.toSystemID);return t},[e.edges]),_=(0,l.useMemo)(()=>e.systems.filter(e=>g.has(e.systemID)),[g,e.systems]),v=(0,l.useMemo)(()=>se(_),[_]),y=(0,l.useMemo)(()=>new Map(e.systems.map(e=>[e.systemID,e])),[e.systems]),[b,x]=(0,l.useState)(()=>ce(v,m)),S=(0,l.useMemo)(()=>ce(v,m),[v,m]);(0,l.useEffect)(()=>{let e=d.current;if(!e)return;let t=new ResizeObserver(([t])=>{let n=Math.max(1,Math.round(t.contentRect.width)),r=Math.max(1,Math.round(t.contentRect.height)),i=Math.min(2,window.devicePixelRatio||1);e.width=Math.round(n*i),e.height=Math.round(r*i),h({width:n,height:r,dpr:i})});return t.observe(e),()=>t.disconnect()},[]),(0,l.useEffect)(()=>{x(S)},[S,e.version]);let C=(0,l.useCallback)(()=>x(S),[S]),w=(0,l.useCallback)(e=>Math.max(S.scale*re,Math.min(S.scale*O,e)),[S.scale]),ee=(0,l.useCallback)((e,t)=>{x(n=>{let r=w(Math.max(n.scale*k,S.scale*ie));return{scale:r,x:m.width/2-e*r,y:m.height/2-t*r}})},[w,S.scale,m.height,m.width]),te=(0,l.useCallback)(e=>{let t=v.points.get(e);t&&ee(t.x,t.y)},[ee,v.points]);(0,l.useEffect)(()=>{s&&te(s.systemID)},[s,te]),(0,l.useEffect)(()=>{let s=d.current;if(!s||m.width<=1||m.height<=1)return;let l=s.getContext(`2d`);if(!l)return;let{dpr:u,width:f,height:p}=m;l.setTransform(u,0,0,u,0,0),l.clearRect(0,0,f,p);let h=e=>({x:e.x*b.scale+b.x,y:e.y*b.scale+b.y}),g=(e,t=20)=>e.x>=-t&&e.y>=-t&&e.x<=f+t&&e.y<=p+t;l.save(),l.lineWidth=.65,l.strokeStyle=`rgba(103, 150, 161, 0.17)`,l.beginPath();for(let t of e.edges){let e=v.points.get(t.fromSystemID),n=v.points.get(t.toSystemID);if(!e||!n)continue;let r=h(e),i=h(n);!g(r,80)&&!g(i,80)||(l.moveTo(r.x,r.y),l.lineTo(i.x,i.y))}l.stroke(),r?.systemIDs.length&&(l.lineJoin=`round`,l.lineCap=`round`,l.lineWidth=5,l.strokeStyle=`rgba(36, 166, 193, 0.18)`,l.beginPath(),r.systemIDs.forEach((e,t)=>{let n=v.points.get(e);if(!n)return;let r=h(n);t===0?l.moveTo(r.x,r.y):l.lineTo(r.x,r.y)}),l.stroke(),l.lineWidth=1.7,l.strokeStyle=`rgba(119, 232, 210, 0.92)`,l.stroke());let x=new Set(n.map(e=>e.systemID).filter(e=>e!==null));for(let e of _){let n=v.points.get(e.systemID);if(!n)continue;let r=h(n);if(!g(r))continue;let a=e.systemID===i||e.systemID===t.characterSystemID||x.has(e.systemID),o=a?3.3:b.scale>S.scale*4?2:1.25;l.beginPath(),l.arc(r.x,r.y,o,0,Math.PI*2),l.fillStyle=ae(e.securityStatus,a?.95:.72),l.fill()}if(t.characterSystemID!==null){let e=v.points.get(t.characterSystemID);if(e){let t=h(e);l.beginPath(),l.arc(t.x,t.y,7.5,0,Math.PI*2),l.strokeStyle=`rgba(236, 249, 251, 0.82)`,l.lineWidth=1,l.stroke()}}let C=new Map;for(let e of n)e.systemID!==null&&C.set(e.systemID,[...C.get(e.systemID)||[],e]);for(let[e,t]of C){let n=v.points.get(e);if(!n)continue;let r=h(n);t.slice(0,5).forEach((e,t)=>{let n=9+t*6,i=e.operationID===o||e.crews.some(e=>e.assetID===a);l.beginPath(),l.moveTo(r.x+n,r.y-5),l.lineTo(r.x+n+4,r.y+3),l.lineTo(r.x+n-4,r.y+3),l.closePath(),l.fillStyle=i?`rgba(118, 235, 183, 1)`:`rgba(118, 235, 183, 0.7)`,l.fill()}),t.length>5&&(l.font=`600 8px ui-monospace, SFMono-Regular, Menlo, monospace`,l.fillStyle=`rgba(118, 235, 183, 0.8)`,l.fillText(`+${t.length-5}`,r.x+40,r.y+3))}if(i!==null){let e=v.points.get(i);if(e){let t=h(e);l.beginPath(),l.arc(t.x,t.y,10,0,Math.PI*2),l.strokeStyle=`rgba(115, 224, 244, 0.95)`,l.lineWidth=1.25,l.stroke(),l.beginPath(),l.moveTo(t.x-14,t.y),l.lineTo(t.x-8,t.y),l.moveTo(t.x+8,t.y),l.lineTo(t.x+14,t.y),l.stroke()}}let w=new Set([...i===null?[]:[i],...t.characterSystemID===null?[]:[t.characterSystemID],...C.keys()]),ee=(e,t)=>{let n=v.points.get(e),r=y.get(e);if(!n||!r)return!1;let i=h(n);return g(i,70)?(l.font=`${t?600:500} ${t?10:8}px ui-monospace, SFMono-Regular, Menlo, monospace`,l.fillStyle=t?`rgba(216, 239, 244, 0.96)`:`rgba(127, 159, 168, 0.74)`,l.fillText(r.systemName,i.x+7,i.y-7),!0):!1};for(let e of w)ee(e,!0);if(b.scale>S.scale*5){let e=0;for(let t of _)if(!w.has(t.systemID)&&(ee(t.systemID,!1)&&(e+=1),e>=220))break}c&&(l.font=`700 34px ui-monospace, SFMono-Regular, Menlo, monospace`,l.textAlign=`center`,l.fillStyle=`rgba(100, 216, 238, 0.055)`,l.fillText(`DEMO TOPOLOGY`,f/2,p-34),l.textAlign=`start`),l.restore()},[b,c,_,S.scale,n,v.points,r,a,o,i,t,y,e,m]);let ne=(0,l.useCallback)((e,t,n)=>{x(r=>{let i=w(r.scale*n),a=(e-r.x)/r.scale,o=(t-r.y)/r.scale;return{scale:i,x:e-a*i,y:t-o*i}})},[w]),T=(0,l.useCallback)((e,t,n=16)=>{let r=null,i=n;for(let n of v.points.values()){let a=n.x*b.scale+b.x,o=n.y*b.scale+b.y,s=Math.hypot(a-e,o-t);s{let n=d.current;if(!n)return null;let r=n.getBoundingClientRect();return{x:e-r.left,y:t-r.top}},[]),j=(0,l.useCallback)((e,t)=>{let n=E(e,t);if(!n)return;let r=T(n.x,n.y);r!==null&&u(r)},[E,T,u]),M=(0,l.useCallback)((e,t)=>{let n=E(e,t);if(!n)return;let r=T(n.x,n.y,22);if(r!==null){u(r),te(r);return}ee((n.x-b.x)/b.scale,(n.y-b.y)/b.scale)},[b,E,ee,te,T,u]);return(0,D.jsxs)(`div`,{className:`universe-map-stage`,children:[(0,D.jsx)(`canvas`,{ref:d,className:`universe-canvas`,tabIndex:0,role:`img`,"aria-label":`Interactive connected stargate map with ${_.length.toLocaleString()} systems. Drag to pan, use the mouse wheel or plus and minus keys to zoom, and select a system by clicking its node. Double-click or double-tap an area to zoom deeply and center it. Press Enter to focus the selected destination and F to fit the empire. A searchable system list follows the map.`,onPointerDown:e=>{e.button===0&&(e.currentTarget.setPointerCapture(e.pointerId),f.current={active:!0,pointerID:e.pointerId,x:e.clientX,y:e.clientY,moved:!1})},onPointerMove:e=>{let t=f.current;if(!t.active||t.pointerID!==e.pointerId)return;let n=e.clientX-t.x,r=e.clientY-t.y;Math.abs(n)+Math.abs(r)>2&&(t.moved=!0),t.x=e.clientX,t.y=e.clientY,x(e=>({...e,x:e.x+n,y:e.y+r}))},onPointerUp:e=>{let t=f.current;if(!(!t.active||t.pointerID!==e.pointerId)){if(!t.moved&&(j(e.clientX,e.clientY),e.pointerType===`touch`)){let t=Date.now(),n=p.current;t-n.at<=350&&Math.hypot(e.clientX-n.x,e.clientY-n.y)<=24?(M(e.clientX,e.clientY),p.current={at:0,x:0,y:0}):p.current={at:t,x:e.clientX,y:e.clientY}}t.active=!1,e.currentTarget.releasePointerCapture(e.pointerId)}},onPointerCancel:()=>{f.current.active=!1},onDoubleClick:e=>{e.preventDefault(),M(e.clientX,e.clientY)},onWheel:e=>{e.preventDefault();let t=e.currentTarget.getBoundingClientRect();ne(e.clientX-t.left,e.clientY-t.top,Math.exp(-e.deltaY*.0025))},onKeyDown:e=>{if(e.key===`+`||e.key===`=`)e.preventDefault(),ne(m.width/2,m.height/2,A);else if(e.key===`-`)e.preventDefault(),ne(m.width/2,m.height/2,1/A);else if(e.key===`0`||e.key.toLowerCase()===`f`||e.key===`Home`)e.preventDefault(),C();else if(e.key===`Enter`&&i!==null)e.preventDefault(),te(i);else if([`ArrowLeft`,`ArrowRight`,`ArrowUp`,`ArrowDown`].includes(e.key)){e.preventDefault();let t=e.shiftKey?135:45,n=e.key===`ArrowLeft`?t:e.key===`ArrowRight`?-t:0,r=e.key===`ArrowUp`?t:e.key===`ArrowDown`?-t:0;x(e=>({...e,x:e.x+n,y:e.y+r}))}}}),(0,D.jsxs)(`div`,{className:`universe-map-controls`,"aria-label":`Map controls`,children:[(0,D.jsx)(`button`,{type:`button`,"aria-label":`Zoom in`,onClick:()=>ne(m.width/2,m.height/2,A),children:`+`}),(0,D.jsx)(`button`,{type:`button`,"aria-label":`Zoom out`,onClick:()=>ne(m.width/2,m.height/2,1/A),children:`−`}),(0,D.jsx)(`button`,{type:`button`,"aria-label":`Fit all systems`,onClick:C,children:`FIT`})]}),(0,D.jsxs)(`div`,{className:`universe-map-legend`,"aria-hidden":`true`,children:[(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`i`,{className:`legend-high`}),`High security`]}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`i`,{className:`legend-low`}),`Low security`]}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`i`,{className:`legend-null`}),`Null security`]}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`i`,{className:`legend-owned`}),`Owned operation`]})]})]})}function de({children:e,tone:t}){return(0,D.jsxs)(`span`,{className:`status-pill status-${t}`,children:[(0,D.jsx)(`i`,{}),e]})}function fe({bridge:e,bridgeBusy:t,refreshRevision:n,notify:r,onAction:i}){let a=e.mode===`preview`,[o,s]=(0,l.useState)(a?`preview`:`loading`),[c,u]=(0,l.useState)(a?j:null),[d,f]=(0,l.useState)(a?M:{generatedAtMs:null,characterSystemID:null,assets:[]}),[p,m]=(0,l.useState)(``),[h,g]=(0,l.useState)(``),[_,v]=(0,l.useState)(a?99000004:null),[y,b]=(0,l.useState)(a?`demo-miner`:null),[x,S]=(0,l.useState)(a?`site:99200002`:null),[C,w]=(0,l.useState)(null),[E,re]=(0,l.useState)(a?99100004:null),[O,ie]=(0,l.useState)(null),[k,A]=(0,l.useState)(!1),[se,ce]=(0,l.useState)(``),[fe,pe]=(0,l.useState)(`idle`),me=(0,l.useRef)(0),he=(0,l.useRef)(0),ge=(0,l.useRef)(0),_e=(0,l.useRef)(!1),ve=(0,l.useRef)(``),ye=fe!==`idle`,be=(0,l.useCallback)(async t=>{if(e.mode!==`connected`)return null;let n=++me.current,r=await P(e.apiBase,e.token,`/api/v1/universe/snapshot`,t);if(n!==me.current)return null;let i=te(r);return f(i),i},[e.apiBase,e.mode,e.token]);(0,l.useEffect)(()=>{if(e.mode===`preview`){me.current+=1,u(j),f(M),s(`preview`),m(``),v(e=>e&&j.systems.some(t=>t.systemID===e)?e:99000004),b(e=>e&&M.assets.some(t=>t.assetID===e)?e:`demo-miner`),S(`site:99200002`);return}if(e.mode!==`connected`){me.current+=1,u(null),f({generatedAtMs:null,characterSystemID:e.data?.character.solarSystemID||null,assets:[]}),s(e.mode===`error`?`error`:`loading`),m(e.mode===`error`?`Use a fresh /x link from the active EveJS character.`:`Waiting for the character link.`);return}let t=new AbortController,n=!1;s(`loading`),m(`Loading the stargate topology and character-authorized operations.`),ie(null),ce(``),(async()=>{let[n,r]=await Promise.allSettled([P(e.apiBase,e.token,`/api/v1/universe/topology`,t.signal),be(t.signal)]);if(!t.signal.aborted){if(n.status===`rejected`){u(null),s(`error`),m(n.reason instanceof Error?n.reason.message:`The Universe topology could not be loaded.`);return}try{let e=ee(n.value);u(e),s(`ready`),r.status===`rejected`?m(r.reason instanceof Error?`Map loaded; owned-operation telemetry is unavailable: ${r.reason.message}`:`Map loaded; owned-operation telemetry is unavailable.`):m(``)}catch(e){u(null),s(`error`),m(e instanceof Error?e.message:`The Universe topology was not usable.`)}}})();let r=window.setInterval(()=>{n||t.signal.aborted||(n=!0,be(t.signal).catch(e=>{t.signal.aborted||m(e instanceof Error?`Live crew update delayed: ${e.message}`:`Live crew update delayed.`)}).finally(()=>{n=!1}))},1e4);return()=>{t.abort(),window.clearInterval(r)}},[e.apiBase,e.data?.character.solarSystemID,e.mode,e.token,be,n]);let xe=(0,l.useMemo)(()=>new Map((c?.systems||[]).map(e=>[e.systemID,e])),[c]),Se=(0,l.useMemo)(()=>new Map((c?.stations||[]).map(e=>[e.stationID,e])),[c]),Ce=(0,l.useMemo)(()=>ne(d.assets).map(e=>{let t=e.systemID===null?e.systemName:xe.get(e.systemID)?.systemName||e.systemName,n=e.destinationSystemID===null?``:xe.get(e.destinationSystemID)?.systemName||`System ${e.destinationSystemID}`;return{...e,systemName:t,name:e.operationKind===`transit`?`Transit to ${n}`:e.operationKind===`system`&&t?`${t} operation`:e.name}}),[d.assets,xe]),we=(0,l.useMemo)(()=>{let e=new Set;for(let t of c?.edges||[])e.add(t.fromSystemID),e.add(t.toSystemID);return e},[c]),Te=we.size,Ee=(0,l.useMemo)(()=>{let e=new Map;for(let t of c?.stations||[])e.set(t.systemID,[...e.get(t.systemID)||[],t]);for(let t of e.values())t.sort((e,t)=>e.stationName.localeCompare(t.stationName));return e},[c]),De=_===null?null:xe.get(_)||null,Oe=d.assets.find(e=>e.assetID===y)||null,F=E===null?null:Se.get(E)||null,ke=le(Oe,d,Se),Ae=ke===null?null:xe.get(ke)||null,je=`${y||``}|${ke??``}|${_??``}|${E??``}`;(0,l.useEffect)(()=>{ve.current=je},[je]),(0,l.useEffect)(()=>{b(e=>e&&d.assets.some(t=>t.assetID===e)?e:null)},[d.assets]),(0,l.useEffect)(()=>{S(e=>e&&Ce.some(t=>t.operationID===e)?e:y?Ce.find(e=>e.crews.some(e=>e.assetID===y))?.operationID??null:null)},[Ce,y]),(0,l.useEffect)(()=>{if(_===null){re(null);return}let e=Ee.get(_)||[];re(t=>t&&e.some(e=>e.stationID===t)?t:e[0]?.stationID??null)},[_,Ee]),(0,l.useEffect)(()=>{he.current+=1,ie(null),ce(``)},[ke,_]),(0,l.useEffect)(()=>{if(_!==null&&xe.has(_)&&we.has(_))return;let e=d.characterSystemID&&xe.has(d.characterSystemID)&&we.has(d.characterSystemID)?d.characterSystemID:c?.systems.find(e=>we.has(e.systemID))?.systemID??null;v(e)},[we,_,d.characterSystemID,xe,c]);let Me=h.trim().toLocaleLowerCase(),Ne=(0,l.useMemo)(()=>{let e=(c?.systems||[]).filter(e=>we.has(e.systemID));if(Me)return e.filter(e=>e.systemName.toLocaleLowerCase().includes(Me)||String(e.systemID).includes(Me)).sort((e,t)=>e.systemName.localeCompare(t.systemName)).slice(0,100);let t=new Set([..._===null?[]:[_],...d.characterSystemID===null?[]:[d.characterSystemID],...d.assets.map(e=>e.systemID).filter(e=>e!==null)]),n=e.filter(e=>t.has(e.systemID)),r=e.filter(e=>!t.has(e.systemID)).sort((e,t)=>e.systemName.localeCompare(t.systemName));return[...n,...r].slice(0,60)},[we,Me,_,d.assets,d.characterSystemID,c]),Pe=(0,l.useCallback)(e=>{v(e)},[]),Fe=(0,l.useCallback)(e=>{S(e.operationID),b(t=>t&&e.crews.some(e=>e.assetID===t)?t:null)},[]),Ie=(0,l.useCallback)(e=>{e.systemID!==null&&(ge.current+=1,w({systemID:e.systemID,revision:ge.current}))},[]),Le=(0,l.useCallback)((e,t)=>{S(e.operationID),b(t.assetID)},[]),Re=(0,l.useCallback)(async(t,n)=>{if(!c)throw Error(`The stargate topology is not available.`);let r;if(a){let e=N(c,t,n);if(!e)throw Error(`No demo stargate route connects these systems.`);r=e}else{if(e.mode!==`connected`)throw Error(`Link the active EveJS character before requesting a route.`);r=T(await P(e.apiBase,e.token,`/api/v1/universe/route?from=${encodeURIComponent(t)}&to=${encodeURIComponent(n)}`))}if(r.fromSystemID!==t||r.toSystemID!==n)throw Error(`EveJS returned a route for a different origin or destination. Try the order again.`);return r},[e.apiBase,e.mode,e.token,a,c]),ze=async()=>{if(!De||ke===null||k||ye)return;let e=++he.current;A(!0),ce(``);try{let t=await Re(ke,De.systemID);if(e!==he.current)return;ie(t)}catch(t){if(e!==he.current)return;ie(null),ce(t instanceof Error?t.message:`The route could not be calculated.`)}finally{e===he.current&&A(!1)}},Be=!!(O&&De&&ke!==null&&O.fromSystemID===ke&&O.toSystemID===De.systemID),Ve=Oe?ke===null?`The selected crew does not have a reported navigation origin.`:De?F?a?``:e.mode===`connected`?e.data?.character.online?Oe?.contractID?``:`The selected crew does not have a command contract.`:`The linked character must be online before issuing a crew order.`:`Link the active EveJS character before issuing an order.`:`This system has no station destination available.`:`Select a destination system.`:`Open an owned operation and select a crew.`,He=async()=>{if(!Oe||!F||!De||ke===null)return;if(Ve||t||k||_e.current){r(Ve||`Wait for the current EveJS request to finish.`);return}let e=je,n=Oe,o=F,s=De.systemID,c=ke,l=Be?O:null;_e.current=!0,ce(``);try{if(!l){pe(`routing`);let t=++he.current,n=await Re(c,s);if(ve.current!==e)throw Error(`The crew or destination changed while the route was being checked. Review the selection and try again.`);t===he.current&&ie(n),l=n}if(a){r(`[DEMO] ${n.name} would travel ${l.jumps} jump${l.jumps===1?``:`s`} to ${o.stationName}. No EveJS command was sent.`);return}pe(`sending`),await i(`destination`,{selector:n.contractID,destination:String(o.stationID)})&&await be().catch(()=>null)}catch(e){let t=e instanceof Error?e.message:`The travel route could not be checked.`;ce(t),r(t)}finally{_e.current=!1,pe(`idle`)}},Ue=_===null?[]:Ee.get(_)||[],We=O?.systemIDs.map(e=>xe.get(e)?.systemName||`System ${e}`)||[];return(0,D.jsxs)(`div`,{className:`screen universe-screen`,children:[a?(0,D.jsxs)(`div`,{className:`demo-disclosure universe-disclosure`,role:`note`,children:[(0,D.jsx)(`strong`,{children:`DEMO TOPOLOGY`}),(0,D.jsx)(`span`,{children:`Every name, route, station, and crew on this map is sample data. Demo orders never contact EveJS.`})]}):o===`ready`&&p?(0,D.jsx)(`div`,{className:`universe-live-disclosure universe-disclosure-error`,role:`status`,children:(0,D.jsx)(`span`,{children:p})}):null,c?(0,D.jsxs)(D.Fragment,{children:[(0,D.jsxs)(`section`,{className:`universe-command-grid`,"aria-label":`Universe navigation command`,children:[(0,D.jsxs)(`article`,{className:`panel universe-map-panel`,children:[(0,D.jsx)(`div`,{className:`universe-map-heading`,children:(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`h2`,{children:a?`Sample map`:`New Eden`}),(0,D.jsxs)(`p`,{children:[Te.toLocaleString(),` systems · `,c.edges.length.toLocaleString(),` stargate lanes`]})]})}),(0,D.jsx)(ue,{topology:c,snapshot:d,operations:Ce,route:O,selectedSystemID:_,selectedAssetID:y,selectedOperationID:x,focusRequest:C,demo:a,onSelectSystem:Pe})]}),(0,D.jsxs)(`aside`,{className:`universe-command-rail`,children:[(0,D.jsxs)(`section`,{className:`panel universe-asset-panel`,"aria-labelledby":`owned-operations-title`,children:[(0,D.jsxs)(`div`,{className:`panel-topline`,children:[(0,D.jsx)(`h2`,{id:`owned-operations-title`,children:`Owned operations`}),(0,D.jsx)(`span`,{className:`universe-count`,children:Ce.length})]}),Ce.length?(0,D.jsx)(`div`,{className:`universe-asset-list universe-operation-tree`,role:`tree`,"aria-label":`Owned operations and their crews`,children:Ce.map(e=>{let t=e.operationID===x,n=`${e.crews.length} crew${e.crews.length===1?``:`s`}`;return(0,D.jsxs)(`div`,{className:`universe-operation-node ${t?`expanded`:``}`,children:[(0,D.jsxs)(`button`,{type:`button`,className:`universe-operation-button ${t?`selected`:``}`,role:`treeitem`,"aria-expanded":t,"aria-selected":t,onClick:()=>Fe(e),onDoubleClick:()=>Ie(e),onKeyDown:t=>{t.key===`Enter`&&(t.preventDefault(),Fe(e),Ie(e))},title:e.systemID===null?`Open this operation`:`Open this operation; double-click to center it on the map`,children:[(0,D.jsx)(`span`,{className:`operation-marker`,"aria-hidden":`true`,children:`◆`}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:e.name}),(0,D.jsxs)(`small`,{children:[e.systemName||`Location not assigned`,` · `,n]})]}),(0,D.jsx)(`em`,{"aria-hidden":`true`,children:t?`▾`:`›`})]}),t&&(0,D.jsx)(`div`,{className:`universe-crew-tree`,role:`group`,"aria-label":`Crews assigned to ${e.name}`,children:e.crews.map(t=>{let n=t.assetID===y,r=t.hullCount&&t.hullCount>0?`${t.hullCount} hull${t.hullCount===1?``:`s`}`:`${oe(t.role)} crew`;return(0,D.jsxs)(`button`,{type:`button`,role:`treeitem`,"aria-selected":n,className:`universe-crew-button ${n?`selected`:``}`,onClick:()=>Le(e,t),children:[(0,D.jsx)(`span`,{className:`crew-branch-marker`,"aria-hidden":`true`,children:`└`}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:t.name}),(0,D.jsxs)(`small`,{children:[oe(t.state),` · `,r]})]}),(0,D.jsxs)(`em`,{children:[oe(t.role),` crew`]})]},t.assetID)})})]},e.operationID)})}):(0,D.jsxs)(`div`,{className:`universe-empty`,children:[(0,D.jsx)(`strong`,{children:`No owned operations reported`}),(0,D.jsx)(`span`,{children:`The map will not invent markers for contracts EveJS did not return.`})]})]}),(0,D.jsxs)(`section`,{className:`panel universe-target-panel`,"aria-labelledby":`selected-system-title`,children:[(0,D.jsxs)(`div`,{className:`panel-topline`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`Selected destination`}),(0,D.jsx)(`h2`,{id:`selected-system-title`,children:De?.systemName||`No system selected`})]}),De&&(0,D.jsx)(de,{tone:De.securityStatus===null?`muted`:De.securityStatus>=.5?`blue`:De.securityStatus>0?`amber`:`muted`,children:De.securityStatus===null?`Unknown`:De.securityStatus.toFixed(1)})]}),De?(0,D.jsxs)(D.Fragment,{children:[(0,D.jsxs)(`dl`,{className:`universe-system-facts`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`System ID`}),(0,D.jsx)(`dd`,{children:De.systemID})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Region`}),(0,D.jsx)(`dd`,{children:De.regionName||De.regionID||`Not reported`})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Route origin`}),(0,D.jsx)(`dd`,{children:Ae?.systemName||`Crew location not reported`})]})]}),(0,D.jsxs)(`label`,{className:`universe-station-select`,htmlFor:`universe-station`,children:[(0,D.jsx)(`span`,{children:`Destination station`}),(0,D.jsxs)(`select`,{id:`universe-station`,value:E??``,disabled:!Ue.length||k||ye,onChange:e=>re(Number(e.target.value)||null),children:[!Ue.length&&(0,D.jsx)(`option`,{value:``,children:`No station in this system`}),Ue.map(e=>(0,D.jsx)(`option`,{value:e.stationID,children:e.stationName},e.stationID))]})]}),(0,D.jsxs)(`div`,{className:`universe-route-actions`,children:[(0,D.jsx)(`button`,{className:`button ghost`,disabled:k||ye||ke===null,onClick:()=>void ze(),children:k?`Calculating…`:`Inspect route`}),(0,D.jsx)(`button`,{className:`button primary`,disabled:!!Ve||t||k||ye,title:Ve||(Be?`Send this station destination to the selected crew`:`Check the server route, then send this destination`),onClick:()=>void He(),children:fe===`routing`?`Checking route…`:fe===`sending`?`Sending order…`:a?`Demo travel order`:`Issue travel order`})]}),Ve?(0,D.jsx)(`p`,{className:`universe-route-guidance universe-route-guidance-blocked`,role:`status`,children:Ve}):null,se&&(0,D.jsx)(`p`,{className:`universe-route-error`,role:`alert`,children:se})]}):(0,D.jsxs)(`div`,{className:`universe-empty`,children:[(0,D.jsx)(`strong`,{children:`Select a solar system`}),(0,D.jsx)(`span`,{children:`Use the map or searchable list to choose a destination.`})]})]})]})]}),(0,D.jsxs)(`section`,{className:`universe-lower-grid`,children:[(0,D.jsxs)(`article`,{className:`panel universe-route-panel`,"aria-labelledby":`route-preview-title`,children:[(0,D.jsxs)(`div`,{className:`panel-topline`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`Server route`}),(0,D.jsx)(`h2`,{id:`route-preview-title`,children:O?`${O.jumps} jump${O.jumps===1?``:`s`}`:`No route previewed`})]}),O&&(0,D.jsx)(de,{tone:`green`,children:`Calculated`})]}),O?(0,D.jsx)(`ol`,{className:`universe-route-steps`,"aria-label":`Route systems`,children:We.map((e,t)=>(0,D.jsxs)(`li`,{children:[(0,D.jsx)(`span`,{children:String(t+1).padStart(2,`0`)}),(0,D.jsx)(`strong`,{children:e}),t===0?(0,D.jsx)(`em`,{children:`Origin`}):t===We.length-1?(0,D.jsx)(`em`,{children:`Destination`}):null]},`${O.systemIDs[t]}-${t}`))}):(0,D.jsx)(`div`,{className:`universe-empty`,children:(0,D.jsx)(`span`,{children:`Select a crew and destination to inspect its route.`})})]}),(0,D.jsxs)(`article`,{className:`panel universe-system-browser`,"aria-labelledby":`system-browser-title`,children:[(0,D.jsxs)(`div`,{className:`panel-topline`,children:[(0,D.jsx)(`h2`,{id:`system-browser-title`,children:`Find a system`}),Me&&(0,D.jsx)(`span`,{className:`universe-count`,children:Ne.length})]}),(0,D.jsxs)(`form`,{className:`universe-search`,onSubmit:e=>{e.preventDefault(),Ne[0]&&Pe(Ne[0].systemID)},children:[(0,D.jsx)(`label`,{htmlFor:`universe-system-search`,children:`Search system name or ID`}),(0,D.jsx)(`div`,{children:(0,D.jsx)(`input`,{id:`universe-system-search`,type:`search`,value:h,onChange:e=>g(e.target.value),placeholder:`Search New Eden`,autoComplete:`off`})})]}),Me&&(0,D.jsxs)(`p`,{className:`universe-result-note`,children:[`Showing up to 100 matches across `,Te.toLocaleString(),` systems.`]}),(0,D.jsxs)(`div`,{className:`universe-system-list`,role:`listbox`,"aria-label":`Searchable solar systems`,children:[Ne.map(e=>{let t=e.systemID===_,n=Ee.get(e.systemID)?.length||0;return(0,D.jsxs)(`button`,{role:`option`,"aria-selected":t,className:t?`selected`:``,onClick:()=>Pe(e.systemID),children:[(0,D.jsx)(`i`,{style:{backgroundColor:ae(e.securityStatus)},"aria-hidden":`true`}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:e.systemName}),(0,D.jsxs)(`small`,{children:[e.regionName||`Region ${e.regionID||`not reported`}`,` · `,n,` station`,n===1?``:`s`]})]}),(0,D.jsx)(`em`,{children:e.securityStatus===null?`—`:e.securityStatus.toFixed(1)})]},e.systemID)}),!Ne.length&&(0,D.jsxs)(`div`,{className:`universe-empty`,children:[(0,D.jsx)(`strong`,{children:`No matching systems`}),(0,D.jsx)(`span`,{children:`Try part of the name or the numeric system ID.`})]})]})]})]})]}):(0,D.jsxs)(`section`,{className:`panel empty-state telemetry-state universe-load-state`,"aria-live":`polite`,children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:o===`error`?`Universe adapter unavailable`:`Establishing map projection`}),(0,D.jsx)(`h2`,{children:o===`error`?`The empire map could not be opened`:`Loading the stargate topology`}),(0,D.jsx)(`p`,{children:p||`X Command is waiting for the EveJS topology endpoint.`})]})]})}var pe=`x-command.link`;function me(e){let t=new URL(e),n=t.protocol===`http:`&&[`localhost`,`127.0.0.1`,`::1`].includes(t.hostname);if(t.protocol!==`https:`&&!n)throw Error(`The EveJS bridge must use HTTPS.`);if(t.username||t.password)throw Error(`The EveJS bridge address cannot contain credentials.`);if(t.pathname!==`/`||t.search||t.hash)throw Error(`The EveJS bridge address must be an origin without a path, query, or fragment.`);if(typeof window<`u`&&t.origin!==window.location.origin)throw Error(`This bundled X Command interface must use its own EveJS server.`);return t.origin}var he=_();function ge(){return typeof crypto<`u`&&typeof crypto.randomUUID==`function`?crypto.randomUUID():`xc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}var _e=[{label:`Command`,items:[{id:`overview`,label:`Operations`,short:`OP`},{id:`contracts`,label:`Contracts`,short:`CT`},{id:`universe`,label:`Universe map`,short:`UM`}]},{label:`Assets`,items:[{id:`industrial`,label:`Industrial Crews`,short:`IN`},{id:`mercenaries`,label:`Mercenary Outfits`,short:`MC`},{id:`production`,label:`Production`,short:`PR`},{id:`logistics`,label:`Logistics`,short:`LG`},{id:`salvage`,label:`Salvage & Recovery`,short:`SR`}]},{label:`System`,items:[{id:`settings`,label:`Connection & Modules`,short:`CM`}]}],ve={retriever:`https://images.evetech.net/types/17478/render?size=512`,miasmos:`https://images.evetech.net/types/656/render?size=512`,venture:`https://images.evetech.net/types/32880/render?size=512`,porpoise:`https://images.evetech.net/types/42244/render?size=512`,rorqual:`https://images.evetech.net/types/28352/render?size=512`,catalyst:`https://images.evetech.net/types/16240/render?size=512`,moa:`https://images.evetech.net/types/623/render?size=512`},ye=[{name:`Kaalakiota mineral request`,type:`Supply`,place:`Uitra VI`,reward:`6.8M ISK`,time:`05:42:18`},{name:`Wiyrkomi priority haul`,type:`Logistics`,place:`Jita IV`,reward:`3.2M ISK`,time:`02:16:04`},{name:`Unregistered battlefield claim`,type:`Salvage`,place:`Ahtulaima`,reward:`Claim rights`,time:`00:48:31`}];function be({label:e,value:t,detail:n,tone:r=`blue`}){return(0,D.jsxs)(`div`,{className:`metric metric-${r}`,children:[(0,D.jsx)(`div`,{className:`metric-label`,children:e}),(0,D.jsx)(`div`,{className:`metric-value`,children:t}),(0,D.jsx)(`div`,{className:`metric-detail`,children:n})]})}function xe({children:e,tone:t=`green`}){return(0,D.jsxs)(`span`,{className:`status-pill status-${t}`,children:[(0,D.jsx)(`i`,{}),e]})}function Se({value:e,tone:t=`blue`}){return(0,D.jsx)(`div`,{className:`progress`,role:`progressbar`,"aria-label":`Cargo capacity used`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":e,children:(0,D.jsx)(`span`,{className:`progress-${t}`,style:{width:`${e}%`}})})}function Ce({src:e,name:t,className:n=``}){return(0,D.jsxs)(`div`,{className:`ship-portrait ${n}`,children:[e?(0,D.jsx)(D.Fragment,{children:(0,D.jsx)(`img`,{src:e,alt:`${t} ship render`})}):(0,D.jsx)(`span`,{className:`ship-image-unavailable`,role:`img`,"aria-label":`No ship render reported for ${t}`,children:`NO SHIP RENDER`}),(0,D.jsx)(`span`,{className:`image-fade`}),(0,D.jsx)(`span`,{className:`ship-name`,children:t})]})}function we({copy:e,actions:t}){return(0,D.jsxs)(`header`,{className:`module-header`,children:[e&&(0,D.jsx)(`p`,{className:`module-copy`,children:e}),t&&(0,D.jsx)(`div`,{className:`header-actions`,children:t})]})}function Te({bridge:e}){return e.mode===`connected`?null:e.mode===`connecting`?(0,D.jsxs)(`div`,{className:`bridge-banner`,role:`status`,children:[(0,D.jsx)(xe,{tone:`blue`,children:`Connecting`}),(0,D.jsx)(`span`,{children:`Verifying the one-click character link…`})]}):e.mode===`error`?(0,D.jsxs)(`div`,{className:`bridge-banner bridge-error`,role:`alert`,children:[(0,D.jsx)(xe,{tone:`amber`,children:`Link needed`}),(0,D.jsx)(`span`,{children:e.message})]}):(0,D.jsxs)(`div`,{className:`bridge-banner`,role:`note`,children:[(0,D.jsx)(xe,{tone:`muted`,children:`Interface preview`}),(0,D.jsx)(`span`,{children:`Sample data only. Once the server bridge is installed, type /x in game to link the active character.`})]})}function Ee({onNavigate:e,notify:t,bridge:n}){let r=n.mode===`connected`,i=r?n.data?.industrial??null:null,a=!!i?.enabled,o=r?i?.contracts??[]:null,s=o?.find(e=>e.role===`miner`)??null,c=o?.find(e=>e.role===`hauler`)??null,l=o?.reduce((e,t)=>e+t.cargoVolumeM3,0)??0,u=o?.reduce((e,t)=>e+t.cargoCapacityM3,0)??0,d=u>0?Math.min(100,Math.round(l/u*100)):0;return(0,D.jsxs)(`div`,{className:`screen`,children:[(0,D.jsx)(we,{copy:`Review owned crews and issue server-backed orders.`}),(0,D.jsx)(Te,{bridge:n}),(0,D.jsx)(`section`,{className:`metric-grid`,children:r?(0,D.jsxs)(D.Fragment,{children:[(0,D.jsx)(be,{label:`Active crews`,value:i?`${o?.length??0}`:`—`,detail:i?`reported by EveJS`:`industrial state unavailable`,tone:`green`}),(0,D.jsx)(be,{label:`Cargo custody`,value:i?`${l.toLocaleString()} m³`:`—`,detail:i?`${u.toLocaleString()} m³ total capacity`:`industrial state unavailable`})]}):(0,D.jsxs)(D.Fragment,{children:[(0,D.jsx)(be,{label:`Liquid assets`,value:`68.4M`,detail:`demo ISK available to command`}),(0,D.jsx)(be,{label:`Active crews`,value:`2 / 3`,detail:`demo crew capacity`,tone:`green`}),(0,D.jsx)(be,{label:`Ore throughput`,value:`1,248`,detail:`demo m³ per hour`}),(0,D.jsx)(be,{label:`Physical budget`,value:`7 / 48`,detail:`demo Living Universe entities`,tone:`amber`})]})}),(0,D.jsxs)(`section`,{className:`overview-grid`,style:r?{gridTemplateColumns:`1fr`}:void 0,children:[(0,D.jsxs)(`article`,{className:`panel active-operation`,children:[(0,D.jsxs)(`div`,{className:`panel-topline`,children:[(0,D.jsxs)(`div`,{children:[(!r||!i||!a)&&(0,D.jsx)(`p`,{className:`eyebrow`,children:r?i?`Industrial module disabled`:`Industrial state unavailable`:`Demo operation · OP-0417`}),(0,D.jsx)(`h2`,{children:r?i?a?o?.length?`${o.length}-ship industrial wing`:`No crew currently contracted`:`Industrial hirelings are disabled`:`No industrial telemetry reported`:`Uitra extraction wing`})]}),r&&(!i||!a)&&(0,D.jsx)(xe,{tone:`amber`,children:i?`Disabled`:`Unavailable`})]}),(0,D.jsxs)(`div`,{className:`wing-visual`,children:[(0,D.jsx)(Ce,{src:s?.shipTypeID?`https://images.evetech.net/types/${s.shipTypeID}/render?size=512`:r?``:ve.retriever,name:s?.shipName||s?.displayName||(r?i?`No miner contracted`:`Miner not reported`:`Quiet Resolve`)}),(0,D.jsx)(`div`,{className:`link-line`,children:!r&&(0,D.jsx)(`span`,{children:`DEMO 12.4 km`})}),(0,D.jsx)(Ce,{src:c?.shipTypeID?`https://images.evetech.net/types/${c.shipTypeID}/render?size=512`:r?``:ve.miasmos,name:c?.shipName||c?.displayName||(r?i?`No hauler contracted`:`Hauler not reported`:`Long Carry`)})]}),(0,D.jsxs)(`div`,{className:`operation-readout`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`span`,{children:`Miner order`}),(0,D.jsx)(`strong`,{children:s?.order||(r?i?`No miner`:`Not reported`:`Uitra VI · Asteroid Belt 1`)})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`span`,{children:`Hauler order`}),(0,D.jsx)(`strong`,{children:c?.order||(r?i?`No hauler`:`Not reported`:`State War Academy`)})]})]}),(0,D.jsxs)(`div`,{className:`cargo-line`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`span`,{children:`Combined custody`}),(0,D.jsx)(`strong`,{children:r?i?`${l.toLocaleString()} / ${u.toLocaleString()} m³`:`Not reported`:`DEMO 41,860 / 58,000 m³`})]}),(0,D.jsx)(Se,{value:r?i?d:0:72,tone:`green`})]}),(0,D.jsxs)(`div`,{className:`button-row`,children:[(0,D.jsx)(`button`,{className:`button primary`,onClick:()=>e(`industrial`),children:`Manage crews`}),!r&&(0,D.jsx)(`button`,{className:`button danger`,onClick:()=>t(`[DEMO] Both sample ships would be recalled after their current safe action.`),children:`Demo recall`})]})]}),!r&&(0,D.jsxs)(`aside`,{className:`panel signal-feed`,children:[(0,D.jsxs)(`div`,{className:`panel-topline`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`Demo feed`}),(0,D.jsx)(`h2`,{children:`Universe signals`})]}),(0,D.jsx)(`button`,{className:`text-button`,onClick:()=>e(`universe`),children:`View demo feed`})]}),(0,D.jsxs)(`div`,{className:`signal-list`,children:[(0,D.jsxs)(`button`,{onClick:()=>e(`universe`),children:[(0,D.jsx)(`span`,{className:`signal-icon amber`,children:`!`}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:`Demo industrial signature`}),(0,D.jsx)(`small`,{children:`Sample Rorqual-led operation · 2 demo jumps`})]}),(0,D.jsx)(`time`,{children:`DEMO`})]}),(0,D.jsxs)(`button`,{onClick:()=>e(`salvage`),children:[(0,D.jsx)(`span`,{className:`signal-icon blue`,children:`SR`}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:`Battle aftermath detected`}),(0,D.jsx)(`small`,{children:`Claim window closes in 48 minutes`})]}),(0,D.jsx)(`time`,{children:`4m`})]}),(0,D.jsxs)(`button`,{onClick:()=>e(`contracts`),children:[(0,D.jsx)(`span`,{className:`signal-icon green`,children:`C`}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:`New logistics contract`}),(0,D.jsx)(`small`,{children:`Wiyrkomi · 3.2M ISK`})]}),(0,D.jsx)(`time`,{children:`11m`})]})]})]})]}),!r&&(0,D.jsxs)(`section`,{className:`panel contract-strip`,children:[(0,D.jsxs)(`div`,{className:`panel-topline`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`Demo work`}),(0,D.jsx)(`h2`,{children:`Contract exchange`})]}),(0,D.jsx)(`button`,{className:`text-button`,onClick:()=>e(`contracts`),children:`All demo contracts`})]}),(0,D.jsx)(`div`,{className:`table-wrap`,children:(0,D.jsxs)(`table`,{children:[(0,D.jsx)(`thead`,{children:(0,D.jsxs)(`tr`,{children:[(0,D.jsx)(`th`,{children:`Contract`}),(0,D.jsx)(`th`,{children:`Class`}),(0,D.jsx)(`th`,{children:`Location`}),(0,D.jsx)(`th`,{children:`Reward`}),(0,D.jsx)(`th`,{children:`Expires`})]})}),(0,D.jsx)(`tbody`,{children:ye.map(t=>(0,D.jsxs)(`tr`,{children:[(0,D.jsx)(`td`,{children:(0,D.jsxs)(`button`,{className:`table-link`,onClick:()=>e(`contracts`),children:[(0,D.jsx)(`span`,{className:`row-marker`}),t.name]})}),(0,D.jsx)(`td`,{children:t.type}),(0,D.jsx)(`td`,{children:t.place}),(0,D.jsx)(`td`,{className:`isk`,children:t.reward}),(0,D.jsx)(`td`,{className:`mono`,children:t.time})]},t.name))})]})})]})]})}var De={enabled:!0,crewModeEnabled:!0,economyEnabled:!1,crewPackages:[{crewTypeID:`venture_crew`,tier:1,tierLabel:`Tier I`,name:`Venture Crew`,composition:`3 Ventures, 1 Miasmos ore hauler`,hullCount:4,pricing:{mobilizationFeeISK:5e6,refundableLossEscrowISK:25e5,activePayrollISKPerHour:25e4,upfrontISK:75e5},security:{allowedSecurityClasses:[`highsec`,`lowsec`,`nullsec`]}},{crewTypeID:`barge_crew`,tier:2,tierLabel:`Tier II`,name:`Barge Crew`,composition:`3 Retrievers, 1 Porpoise, 1 Miasmos ore hauler`,hullCount:5,pricing:{mobilizationFeeISK:55e6,refundableLossEscrowISK:155e6,activePayrollISKPerHour:15e5,upfrontISK:21e7},security:{allowedSecurityClasses:[`highsec`,`lowsec`,`nullsec`]}}],systems:[{systemID:99000001,systemName:`Demo System Alpha`,securityStatus:.8,securityClass:`highsec`,sites:[{siteID:99000101,siteName:`Demo Asteroid Belt 1`,siteType:`asteroid_belt`,systemID:99000001,status:`assigned`,availability:`assigned`,assignedContractID:`demo-miner`,assignedContractIDs:[`demo-miner`]},{siteID:99000102,siteName:`Demo Asteroid Belt 2`,siteType:`asteroid_belt`,systemID:99000001,status:`unassigned`,availability:`available`,assignedContractID:null,assignedContractIDs:[]}]},{systemID:99000002,systemName:`Demo System Beta`,securityStatus:.5,securityClass:`highsec`,sites:[{siteID:99000201,siteName:`Demo Asteroid Belt 1`,siteType:`asteroid_belt`,systemID:99000002,status:`unassigned`,availability:`available`,assignedContractID:null,assignedContractIDs:[]}]}],contracts:[{contractID:`demo-miner`,serial:17,role:`miner`,state:`active`,order:`mine`,pilotName:`Demo Crew Alpha`,shipName:`Sample Retriever`,displayName:`Sample Retriever`,shipTypeID:17478,cargoStatus:`custody`,cargoVolumeM3:14800,cargoCapacityM3:27500,expiresAtMs:0,phase:`extracting`,assignedSiteID:99000101,assignedSiteName:`Demo Asteroid Belt 1`,assignedSystemName:`Demo System Alpha`},{contractID:`demo-hauler`,serial:18,role:`hauler`,state:`active`,order:`haul`,pilotName:`Demo Crew Beta`,shipName:`Sample Miasmos`,displayName:`Sample Miasmos`,shipTypeID:656,cargoStatus:`custody`,cargoVolumeM3:7600,cargoCapacityM3:42e3,expiresAtMs:0,phase:`collecting`,assignedSiteID:null,assignedSiteName:null,assignedSystemName:null}]};function Oe(e){return e==null?``:String(e)}function F(e,t=`Not reported`){return e?e.replace(/_/g,` `).replace(/\b\w/g,e=>e.toUpperCase()):t}function ke(e){let t=Number(e);return!Number.isFinite(t)||t<0?`Not reported`:`${Math.round(t).toLocaleString(`en-US`)} ISK`}function Ae(e){let t={highsec:`High security`,lowsec:`Low security`,nullsec:`Null security`},n=[...new Set((Array.isArray(e)?e:[]).map(e=>String(e||``).trim().toLowerCase()).filter(Boolean))];return n.length?n.map(e=>t[e]||F(e)).join(` · `):`Not reported`}function je(e){let t=e.toLowerCase();return t?/(recovery|required|failed|error|stranded|paused)/.test(t)?`amber`:/(transit|returning|withdrawing|standby)/.test(t)?`blue`:/(active|mining|extracting|collecting|hauling)/.test(t)?`green`:`muted`:`muted`}function Me(e,t){return t?`Demo contract window`:!Number.isFinite(e)||e<=0?`Not reported`:`${new Date(e).toISOString().slice(0,16).replace(`T`,` `)} UTC`}function Ne(e){let t=e.assignedSystemName||e.systemName||``,n=e.assignedSiteName||``;return n&&t?`${n} / ${t}`:n||t||`Unassigned`}function Pe({ship:e,pilot:t,role:n,image:r,cargoVolume:i,cargoCapacity:a,status:o,details:s,children:c}){let l=a>0?Math.min(100,Math.max(0,Math.round(i/a*100))):0;return(0,D.jsxs)(`article`,{className:`panel crew-card`,children:[(0,D.jsx)(Ce,{src:r,name:e,className:`crew-portrait`}),(0,D.jsxs)(`div`,{className:`crew-body`,children:[(0,D.jsxs)(`div`,{className:`panel-topline compact`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:n}),(0,D.jsx)(`h2`,{children:e}),(0,D.jsxs)(`p`,{className:`pilot-name`,children:[`Crew chief - `,t||`Not reported`]})]}),(0,D.jsx)(xe,{tone:je(o),children:F(o)})]}),(0,D.jsx)(`div`,{className:`crew-stats`,children:s.map(e=>(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`span`,{children:e.label}),(0,D.jsx)(`strong`,{children:e.value})]},e.label))}),(0,D.jsxs)(`div`,{className:`cargo-line`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`span`,{children:`Cargo custody`}),(0,D.jsxs)(`strong`,{children:[i.toLocaleString(),` / `,a.toLocaleString(),` m3`]})]}),(0,D.jsx)(Se,{value:l,tone:l>75?`amber`:`green`})]}),(0,D.jsx)(`div`,{className:`button-row crew-actions`,children:c})]})]})}function Fe({notify:e,bridge:t,onAction:n,bridgeBusy:r}){let[i,a]=(0,l.useState)(``),[o,s]=(0,l.useState)(``),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(``),[p,m]=(0,l.useState)({}),[h,g]=(0,l.useState)(``),_=t.mode===`connected`,v=t.mode===`preview`,y=v||!!t.data?.character.online,b=v||!!t.data?.character.docked,x=_?t.data?.industrial??null:v?De:null,S=t.data?.modules.find(e=>e.id===`industrial`)??null,C=_&&(x?.enabled===!1||S?.enabled===!1),w=x?.contracts??[],ee=x?.systems??[],te=x?.crewModeEnabled===!0,ne=x?.economyEnabled===!0,T=x?.crewPackages??[],E=T.find(e=>e.crewTypeID===d)??T[0]??null,re=w.filter(e=>e.role===`miner`),O=w.filter(e=>e.role===`hauler`),ie=re.filter(e=>e.state.toLowerCase()===`active`),k=w.filter(e=>e.state.toLowerCase()===`active`),A=ee.find(e=>Oe(e.systemID)===i)??ee[0]??null,j=ie.find(e=>e.contractID===o)??ie[0]??null,M=O.find(e=>e.contractID===c)??O[0]??null,ae=M?`${M.destinationStationID??``}:${M.homeStationID??``}:${t.data?.character.dockedLocationID??``}`:``,oe=M?p[M.contractID]:null,se=M?.destinationStationID&&M.destinationStationID===M.homeStationID?`home`:M?.destinationStationID&&M.destinationStationID===t.data?.character.dockedLocationID?`current`:``,ce=oe?.snapshot===ae?oe.value:se,N=v?``:_?C?`Industrial hirelings are disabled in the server configuration.`:x?y?``:`The linked character is offline. Industrial status is read only until that character is online.`:`Industrial state is not available from the EveJS bridge.`:`Link an active EveJS character before issuing industrial orders.`,P=N||(b?``:`Dock the linked character before hiring a crew.`),le=h||r?`Wait for the current command or server refresh to finish.`:``,ue=async(t,r,i,a)=>{if(v)return e(`[DEMO] ${a}`),!0;if(N)return e(N),!1;g(t);try{return await n(r,i)}finally{g(``)}},de=async t=>{if(!j){e(v?`[DEMO] Hire a miner before assigning a site.`:`Hire a miner before assigning a site.`);return}await ue(`assign-${Oe(t.siteID)}`,`assign_site`,{selector:j.contractID,siteID:t.siteID},`${j.shipName||j.displayName} would be assigned to ${t.siteName}.`)},fe=async t=>{if(t){if(!M){e(v?`[DEMO] Hire a hauler before setting a destination.`:`Hire a hauler before setting a destination.`);return}if(t===`current`&&!b){e(`Dock the linked character before selecting the current station as a destination.`);return}await ue(`destination-${M.contractID}`,`destination`,{selector:M.contractID,destination:t},`${M.shipName||M.displayName} would deliver to the ${t===`home`?`home station`:`current station`}.`)&&m(e=>({...e,[M.contractID]:{value:t,snapshot:ae}}))}},pe=async()=>{if(v){e(`[DEMO] Every sample crew would enter standby in sequence.`);return}for(let e of k)if(!await ue(`standby-${e.contractID}`,`order`,{selector:e.contractID,order:`standby`},`${e.shipName||e.displayName} would stand by.`))break},me=async e=>{let t=e.shipName||e.displayName||`Contract #${e.serial}`;!v&&!window.confirm(`Dismiss ${t}? The contract will end and the crew will leave your command.`)||await ue(`dismiss-${e.contractID}`,`dismiss`,{selector:e.contractID},`${t} would be dismissed.`)},he=async()=>{if(!E){e(`EveJS did not report a complete crew package to commission.`);return}await ue(`hire-${E.crewTypeID}`,`hire`,{crewTypeID:E.crewTypeID},`${E.name} would be commissioned${ne?` with server-settled pricing`:` without an ISK charge while crew economy is disabled`}.`)},ge=e=>{let t=new Set(e.sites.map(e=>Oe(e.siteID)));return re.filter(e=>t.has(Oe(e.assignedSiteID))).length},_e=e=>{let t=new Set([...e.assignedContractIDs??[],...e.assignedContractID?[e.assignedContractID]:[]]);return re.filter(n=>t.has(n.contractID)||Oe(n.assignedSiteID)===Oe(e.siteID))},ve=!!h||r||!!N,ye=!!h||r||!!P;return(0,D.jsxs)(`div`,{className:`screen`,"aria-busy":!!h||r,children:[(0,D.jsx)(we,{copy:`Assign mining sites and issue crew orders.`,actions:x&&!te?(0,D.jsxs)(D.Fragment,{children:[(0,D.jsx)(`button`,{className:`button ghost`,disabled:ye,title:le||P||`Hire a mining crew`,onClick:()=>void ue(`hire-miner`,`hire`,{role:`miner`},`A miner contract would be opened.`),children:`Hire miner`}),(0,D.jsx)(`button`,{className:`button primary`,disabled:ye,title:le||P||`Hire a hauling crew`,onClick:()=>void ue(`hire-hauler`,`hire`,{role:`hauler`},`A hauler contract would be opened.`),children:`Hire hauler`})]}):null}),(0,D.jsx)(Te,{bridge:t}),!v&&(N||!b)&&(0,D.jsxs)(`div`,{className:`action-constraint`,role:`status`,children:[(0,D.jsx)(`strong`,{children:N?`Industrial commands unavailable`:`Hiring requires docking`}),(0,D.jsx)(`span`,{children:N||`Remote crew orders remain available, but hiring and the Current station destination require the linked character to be docked.`})]}),x?(0,D.jsxs)(D.Fragment,{children:[te&&(0,D.jsxs)(`section`,{className:`panel crew-commission-panel`,"aria-labelledby":`crew-commission-title`,children:[(0,D.jsxs)(`div`,{className:`crew-commission-topline`,children:[(0,D.jsxs)(`div`,{className:`crew-commission-intro`,children:[(0,D.jsx)(`span`,{className:`command-label`,children:`Commissioning`}),(0,D.jsx)(`strong`,{id:`crew-commission-title`,children:`Complete mining crew`}),(0,D.jsx)(`small`,{children:`Choose one server-authored crew package. Its ships, doctrine, security limits, and quote stay together.`})]}),(0,D.jsxs)(`label`,{htmlFor:`crew-package-select`,children:[(0,D.jsx)(`span`,{children:`Crew package`}),(0,D.jsxs)(`select`,{id:`crew-package-select`,value:E?.crewTypeID??``,disabled:!T.length||!!h,onChange:e=>f(e.target.value),children:[!T.length&&(0,D.jsx)(`option`,{value:``,children:`No crew packages reported`}),T.map(e=>(0,D.jsxs)(`option`,{value:e.crewTypeID,children:[e.name,e.tierLabel?` · ${e.tierLabel}`:``]},e.crewTypeID))]})]}),(0,D.jsx)(`button`,{className:`button primary`,disabled:!E||ye,title:le||P||(ne?`Commission this complete crew using the displayed server pricing`:`Commission this complete crew; the displayed quote is not charged while crew economy is disabled`),onClick:()=>void he(),children:`Commission crew`})]}),E?(0,D.jsxs)(D.Fragment,{children:[(0,D.jsxs)(`dl`,{className:`crew-package-facts`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Tier`}),(0,D.jsx)(`dd`,{children:E.tierLabel||(E.tier>0?`Tier ${E.tier}`:`Not reported`)})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Composition`}),(0,D.jsx)(`dd`,{children:E.composition||`Not reported`})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Hull count`}),(0,D.jsx)(`dd`,{children:E.hullCount>0?E.hullCount.toLocaleString(`en-US`):`Not reported`})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Allowed space`}),(0,D.jsx)(`dd`,{children:Ae(E.security?.allowedSecurityClasses)})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Mobilization`}),(0,D.jsx)(`dd`,{children:ke(E.pricing?.mobilizationFeeISK)})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Refundable loss escrow`}),(0,D.jsx)(`dd`,{children:ke(E.pricing?.refundableLossEscrowISK)})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`dt`,{children:`Payroll / hour`}),(0,D.jsx)(`dd`,{children:ke(E.pricing?.activePayrollISKPerHour)})]}),(0,D.jsxs)(`div`,{className:`crew-package-total`,children:[(0,D.jsx)(`dt`,{children:`Upfront total`}),(0,D.jsx)(`dd`,{children:ke(E.pricing?.upfrontISK)})]})]}),(0,D.jsxs)(`p`,{className:`crew-pricing-note ${ne?`pricing-active`:`pricing-quoted`}`,children:[(0,D.jsx)(`strong`,{children:ne?`Pricing active`:`Quote only — not charged`}),(0,D.jsx)(`span`,{children:ne?`EveJS will settle the displayed costs when the crew is commissioned.`:`Crew economy is disabled on this server. These authored prices are shown for planning, but commissioning does not charge ISK.`})]})]}):(0,D.jsx)(`div`,{className:`crew-package-empty`,children:`EveJS has not reported any complete crew packages.`})]}),(0,D.jsxs)(`div`,{className:`command-band industrial-command-band`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`span`,{className:`command-label`,children:`Bulk controls`}),(0,D.jsx)(`strong`,{children:`Hauler delivery`}),(0,D.jsx)(`small`,{children:`Set one delivery destination or stand down active crews.`})]}),(0,D.jsxs)(`label`,{htmlFor:`hauler-select`,children:[(0,D.jsx)(`span`,{children:`Hauler`}),(0,D.jsxs)(`select`,{id:`hauler-select`,value:M?.contractID??``,disabled:!O.length||!!h,onChange:e=>u(e.target.value),children:[(0,D.jsx)(`option`,{value:``,disabled:!0,children:`No hauler contracted`}),O.map(e=>(0,D.jsx)(`option`,{value:e.contractID,children:e.shipName||e.displayName||`Contract #${e.serial}`},e.contractID))]})]}),(0,D.jsxs)(`label`,{htmlFor:`hauler-destination`,children:[(0,D.jsx)(`span`,{children:`Delivery destination`}),(0,D.jsxs)(`select`,{id:`hauler-destination`,value:ce,title:le||N||`Set this hauler's delivery destination`,disabled:!M||ve,onChange:e=>void fe(e.target.value),children:[(0,D.jsx)(`option`,{value:``,children:`Choose destination`}),(0,D.jsx)(`option`,{value:`home`,children:`Home station`}),(0,D.jsxs)(`option`,{value:`current`,disabled:!b,children:[`Current station`,b?``:` (dock first)`]})]}),(0,D.jsxs)(`small`,{className:`destination-readout`,children:[`Server destination: `,M?.destinationStationID||`not reported`]})]}),(0,D.jsx)(`button`,{className:`button ghost`,disabled:!k.length||ve,title:le||N||(k.length?`Place each active crew on standby in sequence`:`No active crew can accept a standby order`),onClick:()=>void pe(),children:`Standby active`})]}),(0,D.jsxs)(`section`,{className:`industrial-operations`,"aria-labelledby":`site-command-title`,children:[(0,D.jsxs)(`aside`,{className:`panel system-browser`,children:[(0,D.jsx)(`div`,{className:`panel-topline`,children:(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`Operational systems`}),(0,D.jsx)(`h2`,{children:`Select a system`})]})}),ee.length?(0,D.jsx)(`nav`,{className:`system-list`,"aria-label":`Systems with industrial mining sites`,children:ee.map(e=>{let t=A?.systemID===e.systemID,n=e.securityStatus===null||e.securityStatus===void 0?`Security not reported`:`${e.securityStatus.toFixed(1)}${e.securityClass?` ${e.securityClass}`:``}`,r=ge(e);return(0,D.jsxs)(`button`,{className:t?`selected`:``,"aria-pressed":t,onClick:()=>a(Oe(e.systemID)),children:[(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:e.systemName||`System ${e.systemID}`}),(0,D.jsx)(`small`,{children:n})]}),(0,D.jsxs)(`span`,{className:`system-count`,children:[(0,D.jsx)(`strong`,{children:e.sites.length}),(0,D.jsxs)(`small`,{children:[`site`,e.sites.length===1?``:`s`]})]}),(0,D.jsxs)(`span`,{className:`system-count`,children:[(0,D.jsx)(`strong`,{children:r}),(0,D.jsxs)(`small`,{children:[`miner`,r===1?``:`s`]})]})]},e.systemID)})}):(0,D.jsxs)(`div`,{className:`telemetry-empty`,children:[(0,D.jsx)(`strong`,{children:`No mining systems reported`}),(0,D.jsx)(`span`,{children:`EveJS has not returned any charted industrial sites for this character.`})]})]}),(0,D.jsxs)(`div`,{className:`panel site-command-panel`,children:[(0,D.jsxs)(`div`,{className:`site-command-heading`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`Site command`}),(0,D.jsx)(`h2`,{id:`site-command-title`,children:A?.systemName||`No system selected`}),(0,D.jsx)(`p`,{children:A?`${A.sites.length} charted mining site${A.sites.length===1?``:`s`}`:`Select an operational system to inspect its sites.`})]}),(0,D.jsxs)(`label`,{htmlFor:`miner-select`,children:[(0,D.jsx)(`span`,{children:`Active miner to assign or move`}),(0,D.jsxs)(`select`,{id:`miner-select`,value:j?.contractID??``,disabled:!ie.length||!!h,onChange:e=>s(e.target.value),children:[(0,D.jsx)(`option`,{value:``,disabled:!0,children:re.length?`Resume a miner before assignment`:`No miner contracted`}),ie.map(e=>(0,D.jsx)(`option`,{value:e.contractID,children:e.shipName||e.displayName||`Contract #${e.serial}`},e.contractID))]})]})]}),A?.sites.length?(0,D.jsx)(`div`,{className:`site-list`,children:A.sites.map(e=>{let t=_e(e),n=!!(j&&t.some(e=>e.contractID===j.contractID)),r=!!j?.assignedSiteID,i=t.length>0||e.availability===`assigned`||e.status===`assigned`,a=t.length?t.map(e=>e.shipName||e.displayName||`Contract #${e.serial}`).join(`, `):i?`Assigned contract - details unavailable`:`No miner assigned`;return(0,D.jsxs)(`article`,{className:`site-row ${i?`site-assigned`:`site-available`}`,children:[(0,D.jsx)(`span`,{className:`site-marker`,"aria-hidden":`true`,children:i?`IN`:`--`}),(0,D.jsxs)(`div`,{className:`site-identity`,children:[(0,D.jsx)(`span`,{children:F(e.siteType,`Mining site`)}),(0,D.jsx)(`strong`,{children:e.siteName||`Site ${e.siteID}`}),(0,D.jsxs)(`small`,{children:[`Site ID `,e.siteID]})]}),(0,D.jsxs)(`div`,{className:`site-assignment`,children:[(0,D.jsx)(`span`,{children:`Assigned miner`}),(0,D.jsx)(`strong`,{children:a})]}),(0,D.jsx)(xe,{tone:i?`blue`:`green`,children:i?`Assigned`:`Available`}),(0,D.jsx)(`button`,{className:`button primary`,disabled:!j||n||ve,title:le||N||(j?n?`This miner is already assigned here`:`Assign the selected miner to this site`:`Hire a miner before assigning a site`),onClick:()=>void de(e),children:n?`Assigned here`:r?`Move miner here`:`Assign miner`})]},Oe(e.siteID))})}):(0,D.jsxs)(`div`,{className:`telemetry-empty site-empty`,children:[(0,D.jsx)(`strong`,{children:`No charted sites in this system`}),(0,D.jsx)(`span`,{children:`There is currently nowhere in this system that EveJS can assign a mining crew.`})]})]})]}),(0,D.jsxs)(`section`,{className:`crew-roster`,"aria-labelledby":`crew-roster-title`,children:[(0,D.jsxs)(`div`,{className:`section-heading`,children:[(0,D.jsx)(`div`,{children:(0,D.jsx)(`h2`,{id:`crew-roster-title`,children:`Crew roster`})}),(0,D.jsxs)(`span`,{children:[w.length,` reported`]})]}),w.length?(0,D.jsx)(`div`,{className:`crew-grid`,children:w.map(e=>{let t=e.shipName||e.displayName||`Contract #${e.serial}`,n=e.phase||e.state||e.order||``,r=e.state.toLowerCase(),i=r===`active`,a=r===`paused`,o=i||a,s=e.shipTypeID?`https://images.evetech.net/types/${e.shipTypeID}/render?size=512`:``,c=le||N||(i?``:`Resume this contract before issuing work orders.`);return(0,D.jsxs)(Pe,{ship:t,pilot:e.pilotName,role:`${e.role===`miner`?`Mining`:`Hauling`} contract - #${e.serial}`,image:s,cargoVolume:e.cargoVolumeM3,cargoCapacity:e.cargoCapacityM3,status:n,details:[{label:`Assignment`,value:v?`Demo - ${Ne(e)}`:Ne(e)},{label:`Order`,value:F(e.order)},{label:`Expires`,value:Me(e.expiresAtMs,v)}],children:[!i&&(0,D.jsxs)(`span`,{className:`crew-control-note`,role:`status`,children:[`Work orders are unavailable while this contract is `,F(e.state),`.`,a?` Resume it to issue mining, hauling, standby, home, or withdraw orders.`:``]}),(0,D.jsx)(`button`,{className:`button primary`,disabled:ve||!i||e.role===`miner`&&!e.assignedSiteID,title:c||(e.role===`miner`&&!e.assignedSiteID?`Assign this miner to a site before starting work`:`Start this crew's assigned work`),onClick:()=>void ue(`work-${e.contractID}`,`order`,{selector:e.contractID,order:e.role===`miner`?`mine`:`haul`},`${t} would begin its work order.`),children:e.role===`miner`?`Mine assigned site`:`Collect cargo`}),(0,D.jsx)(`button`,{className:`button ghost`,disabled:ve||!o,title:le||N||(o?`${a?`Resume`:`Pause`} this contract`:`This contract cannot be paused or resumed in its current state.`),onClick:()=>void ue(`pause-${e.contractID}`,a?`resume`:`pause`,{selector:e.contractID},`${t} would ${a?`resume`:`pause`}.`),children:a?`Resume`:`Pause`}),(0,D.jsx)(`button`,{className:`button ghost`,disabled:ve||!i,title:c||`Place this crew on standby`,onClick:()=>void ue(`standby-${e.contractID}`,`order`,{selector:e.contractID,order:`standby`},`${t} would stand by.`),children:`Standby`}),(0,D.jsx)(`button`,{className:`button ghost`,disabled:ve||!i,title:c||`Order this crew home`,onClick:()=>void ue(`home-${e.contractID}`,`order`,{selector:e.contractID,order:`home`},`${t} would return home.`),children:`Return home`}),(0,D.jsx)(`button`,{className:`button ghost`,disabled:ve||!i,title:c||`Withdraw this crew safely`,onClick:()=>void ue(`withdraw-${e.contractID}`,`order`,{selector:e.contractID,order:`withdraw`},`${t} would withdraw safely.`),children:`Withdraw`}),(0,D.jsx)(`button`,{className:`button danger`,disabled:ve,title:le||N||`End this crew contract`,onClick:()=>void me(e),children:`Dismiss contract`})]},e.contractID)})}):(0,D.jsxs)(`section`,{className:`panel empty-state`,children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`No active contracts`}),(0,D.jsx)(`h2`,{children:`Hire a miner or hauler while docked.`}),(0,D.jsx)(`p`,{children:`EveJS will report the new contract here. A miner does not begin work until you assign it to a site above.`})]})]})]}):(0,D.jsxs)(`section`,{className:`panel empty-state telemetry-state`,children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`Industrial telemetry unavailable`}),(0,D.jsx)(`h2`,{children:t.mode===`connecting`?`Connecting to EveJS`:C?`Industrial hirelings are disabled`:`No industrial data is available`}),(0,D.jsx)(`p`,{children:t.mode===`error`?`Use a fresh /x link from the active character, then reopen this screen.`:C?S?.reason||`Enable the industrial hirelings module in the server configuration before issuing orders.`:`The bridge has not returned industrial state for this character.`})]})]})}var Ie={mercenaries:{eyebrow:`Asset command · Security division`,title:`Mercenary outfits`,copy:`Contract armed escorts and strike teams where server rules allow them. High-security combat remains locked until its balance rules are proven.`,art:ve.moa,state:`0.0 PILOT PROGRAM`,cards:[{title:`Frontier escort`,meta:`0.0 and low-security only`,detail:`A defensive cruiser follows a designated industrial wing and engages legal aggressors.`,action:`Review doctrine`},{title:`Strike contract`,meta:`Not yet connected`,detail:`Issue an objective rather than micromanaging combat. Losses and ammunition remain real.`,action:`View specification`},{title:`Garrison retainer`,meta:`Living Universe`,detail:`Hire a patrol to protect a player-operated industrial site within the shared NPC budget.`,action:`View specification`}]},production:{eyebrow:`Asset command · Industry network`,title:`Production control`,copy:`Plan manufacturing chains against real station supply, stockpiles, blueprints, and market demand. EveJS owns every material movement and completed job.`,art:ve.porpoise,state:`4 QUEUED JOBS`,cards:[{title:`Retriever hull program`,meta:`42% complete · 1h 18m`,detail:`Uitra VI production line · all minerals reserved in station custody.`,action:`Inspect job`},{title:`Strip Miner I batch`,meta:`Queued · position 2`,detail:`Twelve modules for the Kestrel Reach industrial reserve.`,action:`Inspect job`},{title:`Supply shortfall`,meta:`Needs 18,400 Tritanium`,detail:`Create a mining order, buy order, or logistics contract from the same requirement.`,action:`Source materials`}]},logistics:{eyebrow:`Asset command · Logistics network`,title:`Logistics orders`,copy:`Move cargo through simple destinations and priorities. Orders are intentionally broad so the stock EVE client stays the flight interface.`,art:ve.miasmos,state:`1 ACTIVE ROUTE`,cards:[{title:`Uitra mineral shuttle`,meta:`Long Carry · active`,detail:`Collect from assigned wing, deliver at 80% capacity, repeat until recalled.`,action:`Manage route`},{title:`Station resupply`,meta:`Available`,detail:`Create an auditable station-to-station delivery order using owned stock.`,action:`Create order`},{title:`Emergency recovery`,meta:`Safe-state protocol`,detail:`Separated ships stand down; cargo custody survives disconnects, docking, and jumps.`,action:`View policy`}]},salvage:{eyebrow:`Asset command · Recovery office`,title:`Salvage & recovery`,copy:`Discover simulated battle aftermaths, claim recovery rights, and turn temporary scenes into visible expeditions without leaving wrecks forever.`,art:ve.catalyst,state:`1 CLAIM AVAILABLE`,cards:[{title:`Ahtulaima aftermath`,meta:`Claim expires in 48m`,detail:`Medium battlefield · 14 wreck signatures · probable Guristas involvement.`,action:`Claim site`},{title:`Recovery crew`,meta:`No crew contracted`,detail:`A future civilian salvage team can work a claimed field within the physical entity budget.`,action:`Review contract`},{title:`Cleanup policy`,meta:`Automatic`,detail:`Unclaimed scenes abstract after 25 minutes; claimed sites persist only while meaningfully active.`,action:`View policy`}]},contracts:{eyebrow:`Capsuleer exchange`,title:`Contracts & opportunities`,copy:`A single board for hireling agreements, station requests, production work, salvage claims, and procedural offers generated by the living universe.`,art:ve.venture,state:`3 AVAILABLE`,cards:ye.map(e=>({title:e.name,meta:`${e.type} · ${e.place}`,detail:`Reward: ${e.reward} · offer expires in ${e.time}.`,action:`Review offer`}))}};function Le({section:e,bridge:t}){let n=Ie[e],r=t.mode===`preview`,i=t.data?.modules.find(t=>t.id===e)??null;return(0,D.jsxs)(`div`,{className:`screen`,children:[(0,D.jsx)(we,{copy:n.copy}),(0,D.jsx)(Te,{bridge:t}),r?(0,D.jsx)(D.Fragment,{children:(0,D.jsx)(`section`,{className:`feature-grid`,children:n.cards.map((e,t)=>(0,D.jsxs)(`article`,{className:`panel feature-card`,children:[(0,D.jsxs)(`div`,{className:`card-index`,children:[`0`,t+1]}),(0,D.jsx)(`p`,{className:`eyebrow`,children:e.meta}),(0,D.jsx)(`h2`,{children:e.title}),(0,D.jsx)(`p`,{children:e.detail})]},e.title))})}):(0,D.jsxs)(`section`,{className:`panel empty-state telemetry-state`,children:[(0,D.jsx)(`p`,{className:`eyebrow`,children:`Authoritative adapter status`}),(0,D.jsx)(`h2`,{children:i?.enabled?`No live records are exposed for this module`:`This module is not available`}),(0,D.jsx)(`p`,{children:i?.reason||(i?.enabled?`The server reports this module as enabled, but its live records and controls are not connected to X Command yet.`:`Sample cards are hidden while a character is linked so they cannot be mistaken for live EveJS activity.`)})]})]})}function Re({notify:e,bridge:t,bridgeBusy:n,onResetLink:r}){let i=t.mode===`preview`,a=t.mode===`connected`||!!t.token,o=a&&n,s=t.mode===`connected`?t.data?.character.characterName||`Unreported character`:i?`Astra Vale (Demo)`:`No linked character`,c=new Set([`industrial`,`universe`,`logistics`]),l=[[`industrial`,`Industrial crews`,`Mining and hauling hireling contracts`],[`universe`,`Living Universe`,`Background events and physical scene promotion`],[`production`,`Production control`,`Manufacturing and supply-chain command`],[`logistics`,`Logistics orders`,`Cargo routes and station deliveries`],[`salvage`,`Salvage claims`,`Battle aftermath discovery and cleanup`],[`mercenaries`,`Mercenary outfits`,`Armed hirelings outside high security`]];return(0,D.jsxs)(`div`,{className:`screen`,children:[(0,D.jsx)(we,{copy:`Review the server features available to this character.`}),(0,D.jsx)(Te,{bridge:t}),(0,D.jsxs)(`section`,{className:`settings-layout`,children:[(0,D.jsxs)(`div`,{className:`panel toggle-panel`,children:[(0,D.jsx)(`div`,{className:`panel-topline`,children:(0,D.jsx)(`div`,{children:(0,D.jsx)(`h2`,{children:`Module availability`})})}),i?l.map(([e,t,n])=>(0,D.jsxs)(`div`,{className:`module-status-row`,children:[(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:t}),(0,D.jsx)(`small`,{children:n})]}),(0,D.jsx)(xe,{tone:c.has(e)?`blue`:`muted`,children:c.has(e)?`Enabled`:`Disabled`})]},e)):t.mode===`connected`?l.map(([e,n,r])=>{let i=t.data?.modules.find(t=>t.id===e)??null;return(0,D.jsxs)(`div`,{className:`module-status-row`,children:[(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:n}),(0,D.jsx)(`small`,{children:i?.reason||r})]}),(0,D.jsx)(xe,{tone:i?.enabled?`green`:`muted`,children:i?i.enabled?F(i.state,`Enabled`):`Disabled`:`Not reported`})]},e)}):(0,D.jsxs)(`div`,{className:`telemetry-empty`,children:[(0,D.jsx)(`strong`,{children:`Module manifest unavailable`}),(0,D.jsx)(`span`,{children:`Use a valid /x character link to inspect server feature gates.`})]})]}),(0,D.jsxs)(`aside`,{className:`panel connection-panel`,children:[(0,D.jsx)(`h2`,{children:`Character link`}),(0,D.jsx)(`strong`,{children:s}),(0,D.jsx)(`small`,{children:t.mode===`connected`?`Character session`:i?`Interface preview`:`Link unavailable`}),(0,D.jsx)(`button`,{className:`button ghost`,disabled:o,"aria-disabled":o,title:o?`Wait for the current server request to finish before forgetting this link`:a?`Forget this browser's saved character link`:`Learn how to link a character`,onClick:()=>a?void r():e(`Type /x in game after the bridge patch is installed.`),children:a?`Forget saved link`:`How to link`})]})]})]})}function ze(){let[e,t]=(0,l.useState)(`overview`),[n,r]=(0,l.useState)(``),[i,a]=(0,l.useState)(!1),[o,s]=(0,l.useState)({mode:`preview`,apiBase:``,token:``,data:null,message:``}),[c,u]=(0,l.useState)(!1),[m,h]=(0,l.useState)(!1),[_,v]=(0,l.useState)(0),y=(0,l.useRef)(0),b=(0,l.useRef)(0),x=(0,l.useRef)(0),S=(0,l.useRef)(!1),C=(0,l.useRef)(!1),w=(0,l.useMemo)(()=>_e.flatMap(e=>e.items).find(t=>t.id===e)?.label??`Operations`,[e]),ee=o.mode===`connected`?o.data?.industrial?.contracts.length??0:0,te=o.mode===`connected`&&!!o.data?.character.online,ne=o.mode===`connected`?o.data?.character.solarSystemID||`Unavailable`:o.mode===`preview`?`Preview`:`Unavailable`,T=o.mode===`connected`?o.data?.character.characterName||`Unreported character`:o.mode===`preview`?`Astra Vale`:`No linked character`,E=c||m,re=o.mode===`connected`?{label:te?`Online`:`Offline`,detail:`System ${ne}`,tone:te?`online`:`offline`}:o.mode===`connecting`?{label:`Connecting`,detail:`EveJS bridge`,tone:`connecting`}:o.mode===`error`?{label:`Not linked`,detail:`Use /x in game`,tone:`offline`}:{label:`Preview`,detail:`No active character`,tone:`preview`};(0,l.useEffect)(()=>{if(!n)return;let e=window.setTimeout(()=>r(``),6e3);return()=>window.clearTimeout(e)},[n]);let O=(0,l.useCallback)(e=>{y.current+=1,b.current+=1,x.current+=1,S.current=!1,C.current=!1,u(!1),h(!1),window.localStorage.removeItem(pe),window.localStorage.removeItem(`x-command.api`),window.localStorage.removeItem(`x-command.token`),he.clear(),s({mode:`error`,apiBase:``,token:``,data:null,message:e}),r(e)},[]),ie=(0,l.useCallback)(async(e,t,n=!1)=>{let i=me(e),a=++y.current,o=++b.current,c=n?``:t;s({mode:`connecting`,apiBase:i,token:c,data:null,message:``});try{let e=n?await he.load(i,t):await g(i,t);if(o!==b.current||!f(a,y.current))return;s({mode:`connected`,apiBase:i,token:c,data:e,message:``}),r(`Linked to ${e.character.characterName}.`)}catch(e){if(o!==b.current||!f(a,y.current))return;let t=e instanceof Error?e.message:`The EveJS bridge could not be reached.`;e instanceof d?O(`${t} Use /x in game to create a fresh link.`):s({mode:`error`,apiBase:i,token:c,data:null,message:t})}},[O]);(0,l.useEffect)(()=>{let e=new URLSearchParams(window.location.search),t=new URLSearchParams(window.location.hash.replace(/^#/,``)),n=t.get(`xc_api`)||e.get(`xc_api`)||``,r=t.get(`xc_token`)||e.get(`xc_token`)||``,i=null;try{let e=JSON.parse(window.localStorage.getItem(pe)||`null`);e&&typeof e.apiBase==`string`&&e.apiBase&&(i={apiBase:e.apiBase,token:typeof e.token==`string`?e.token:``})}catch{window.localStorage.removeItem(pe)}let a=window.localStorage.getItem(`x-command.api`)||``,o=window.localStorage.getItem(`x-command.token`)||``,c=p(n,r,i,a,o);if(window.localStorage.removeItem(`x-command.api`),window.localStorage.removeItem(`x-command.token`),e.has(`xc_api`)||e.has(`xc_token`)||t.has(`xc_api`)||t.has(`xc_token`)){e.delete(`xc_api`),e.delete(`xc_token`),t.delete(`xc_api`),t.delete(`xc_token`);let n=e.toString(),r=t.toString();window.history.replaceState(null,``,`${window.location.pathname}${n?`?${n}`:``}${r?`#${r}`:``}`)}try{if(c){let e=me(c.apiBase);window.localStorage.setItem(pe,JSON.stringify({apiBase:e})),queueMicrotask(()=>{ie(e,c.token,!0)})}}catch(e){let t=e instanceof Error?e.message:`The EveJS bridge address is invalid.`;window.localStorage.removeItem(pe),queueMicrotask(()=>{s({mode:`error`,apiBase:``,token:``,data:null,message:t})})}},[ie,O]);let k=(0,l.useCallback)(async(e=!0,t=!1)=>{if(o.mode!==`connected`)return e&&r(`Link a character before refreshing EveJS data.`),`failed`;if(S.current)return e&&r(`A server refresh is already in progress.`),`failed`;if(C.current&&!t)return e&&r(`Wait for the current industrial command to finish before refreshing.`),`failed`;let n=y.current,i=++b.current;S.current=!0,u(!0);try{let t=await g(o.apiBase,o.token);return i!==b.current||!f(n,y.current)?`failed`:(s(e=>e.mode===`connected`?{...e,data:t,message:``}:e),e&&r(`Refreshed EveJS state for ${t.character.characterName}.`),`ok`)}catch(t){if(i!==b.current||!f(n,y.current))return`failed`;let a=t instanceof Error?t.message:`The EveJS bridge could not be refreshed.`;return t instanceof d?(O(`${a} Use /x in game to create a fresh link.`),`unauthorized`):(e&&r(a),`failed`)}finally{i===b.current&&f(n,y.current)&&(S.current=!1,u(!1))}},[o,O]),A=(0,l.useCallback)(async()=>await k(!0)===`ok`,[k]),j=(0,l.useCallback)(async()=>{await A()&&v(e=>e+1)},[A]),M=(0,l.useCallback)(async(e,t={})=>{if(o.mode!==`connected`)return r(`This is preview data. Install the bridge and type /x in game before issuing real orders.`),!1;if(S.current||C.current)return r(`Wait for the current server request to finish before issuing another command.`),!1;C.current=!0,h(!0);let n=y.current,i=b.current,a=++x.current;try{let a={"content-type":`application/json`};o.token&&(a.authorization=`Bearer ${o.token}`);let c=typeof t.commandID==`string`&&t.commandID?t.commandID:ge(),l=await fetch(`${o.apiBase}/api/v1/industrial/action`,{method:`POST`,headers:a,body:JSON.stringify({action:e,...t,commandID:c}),credentials:`include`}),u=await l.json().catch(()=>({}));if(i!==b.current||!f(n,y.current))return!1;if(l.status===401)return O(`${u.error?.message||`This character link has expired.`} Use /x in game to create a fresh link.`),!1;if(!l.ok||u.success!==!0||!u.data)throw Error(u.error?.message||`EveJS rejected the order.`);s(e=>e.data?{...e,data:{...e.data,industrial:u.data||null}}:e);let d=u.message||`EveJS accepted the order.`,p=await k(!1,!0);return!f(n,y.current)||p===`unauthorized`?!1:(r(p===`ok`?d:`${d} The follow-up refresh failed; use Refresh in the top bar before issuing another order.`),!0)}catch(e){if(i!==b.current||!f(n,y.current))return!1;let t=e instanceof Error?e.message:`The order could not be sent to EveJS.`,a=await k(!1,!0);return!f(n,y.current)||a===`unauthorized`||r(a===`ok`?`${t} EveJS state was refreshed.`:`${t} The follow-up refresh also failed.`),!1}finally{a===x.current&&(C.current=!1,h(!1))}},[o,O,k]),ae=async()=>{if(!(!o.apiBase||E)){h(!0);try{let e={};o.token&&(e.authorization=`Bearer ${o.token}`);let t=await fetch(`${o.apiBase}/api/v1/auth/logout`,{method:`POST`,headers:e,credentials:`include`,cache:`no-store`});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.error?.message||`EveJS could not clear this browser session.`)}y.current+=1,b.current+=1,x.current+=1,S.current=!1,C.current=!1,u(!1),window.localStorage.removeItem(pe),window.localStorage.removeItem(`x-command.api`),window.localStorage.removeItem(`x-command.token`),he.clear(),s({mode:`preview`,apiBase:``,token:``,data:null,message:``}),r(`This browser session was cleared. Use /x in game for another one.`)}catch(e){r(e instanceof Error?e.message:`EveJS could not clear this browser session.`)}finally{h(!1)}}},oe=e=>{t(e),a(!1)};return(0,D.jsxs)(`main`,{className:`app-shell`,children:[(0,D.jsx)(`div`,{className:`space-backdrop`}),(0,D.jsxs)(`aside`,{id:`x-command-navigation`,className:`sidebar ${i?`open`:``}`,children:[(0,D.jsxs)(`div`,{className:`brand`,children:[(0,D.jsx)(`div`,{className:`brand-mark`,children:(0,D.jsx)(`span`,{children:`X`})}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`strong`,{children:`X COMMAND`}),(0,D.jsx)(`small`,{children:`CAPSULEER OPERATIONS`})]})]}),(0,D.jsx)(`nav`,{"aria-label":`X Command modules`,children:_e.map(t=>(0,D.jsxs)(`div`,{className:`nav-group`,children:[(0,D.jsx)(`p`,{children:t.label}),t.items.map(t=>{let n=t.id===`industrial`&&ee>0?String(ee):``;return(0,D.jsxs)(`button`,{className:e===t.id?`active`:``,"aria-current":e===t.id?`page`:void 0,onClick:()=>oe(t.id),children:[(0,D.jsx)(`span`,{className:`nav-icon`,children:t.short}),(0,D.jsx)(`span`,{children:t.label}),n&&(0,D.jsx)(`em`,{"aria-label":`${n} active industrial crews`,children:n})]},t.id)})]},t.label))}),(0,D.jsxs)(`footer`,{className:`sidebar-footer sidebar-status connection-${re.tone}`,"aria-live":`polite`,children:[(0,D.jsx)(`i`,{"aria-hidden":`true`}),(0,D.jsxs)(`span`,{children:[(0,D.jsx)(`strong`,{children:re.label}),(0,D.jsx)(`small`,{children:re.detail})]})]})]}),(0,D.jsxs)(`section`,{className:`workspace`,children:[(0,D.jsxs)(`header`,{className:`topbar`,children:[(0,D.jsx)(`button`,{className:`mobile-menu`,"aria-label":`Toggle navigation`,"aria-expanded":i,"aria-controls":`x-command-navigation`,onClick:()=>a(e=>!e),children:`MENU`}),(0,D.jsx)(`h1`,{className:`chrome-title`,children:w}),(0,D.jsxs)(`div`,{className:`chrome-actions`,children:[(0,D.jsx)(`button`,{type:`button`,className:`button ghost chrome-refresh`,disabled:o.mode!==`connected`||E,"aria-busy":c,title:o.mode===`connected`?E?`Wait for the current EveJS request to finish`:`Refresh all live X Command data`:`Link a character to refresh EveJS data`,onClick:()=>void j(),children:`Refresh`}),(0,D.jsx)(`div`,{className:`top-status`,children:(0,D.jsxs)(`div`,{className:`pilot-chip`,children:[(0,D.jsx)(`span`,{className:`avatar`,children:T.split(/\s+/).map(e=>e[0]).join(``).slice(0,2).toUpperCase()}),(0,D.jsx)(`span`,{children:(0,D.jsx)(`strong`,{children:T})})]})})]})]}),(0,D.jsxs)(`div`,{className:`content-scroller`,children:[e===`overview`&&(0,D.jsx)(Ee,{onNavigate:oe,notify:r,bridge:o}),e===`industrial`&&(0,D.jsx)(Fe,{notify:r,bridge:o,onAction:M,bridgeBusy:E}),e===`universe`&&(0,D.jsx)(fe,{notify:r,bridge:o,bridgeBusy:E,refreshRevision:_,onAction:M}),e===`settings`&&(0,D.jsx)(Re,{notify:r,bridge:o,bridgeBusy:E,onResetLink:ae}),e!==`overview`&&e!==`industrial`&&e!==`universe`&&e!==`settings`&&(0,D.jsx)(Le,{section:e,bridge:o})]}),n&&(0,D.jsxs)(`div`,{className:`toast`,role:`status`,"aria-live":`polite`,"aria-atomic":`true`,children:[(0,D.jsx)(`span`,{className:`toast-mark`,children:`X`}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`strong`,{children:`X COMMAND`}),(0,D.jsx)(`p`,{children:n}),(0,D.jsx)(`button`,{type:`button`,className:`text-button`,"aria-label":`Dismiss notification`,onClick:()=>r(``),children:`Dismiss`})]})]},n)]}),i&&(0,D.jsx)(`button`,{className:`nav-scrim`,"aria-label":`Close navigation`,onClick:()=>a(!1)})]})}var Be=document.getElementById(`root`);if(!Be)throw Error(`X Command could not find its application root.`);(0,u.createRoot)(Be).render((0,D.jsx)(l.StrictMode,{children:(0,D.jsx)(ze,{})})); \ No newline at end of file diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/index.html b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/index.html new file mode 100644 index 0000000..f81a94e --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/assets/x-command/index.html @@ -0,0 +1,17 @@ + + + + + + + X Command — Capsuleer Operations + + + + +
+ + diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandBridge.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandBridge.js new file mode 100644 index 0000000..ac0fc3a --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandBridge.js @@ -0,0 +1,566 @@ +"use strict"; + +const assert = require("assert"); +const path = require("path"); + +const { + createAuthStore, + parseToken, +} = require(path.join(__dirname, "../src/services/xCommand/xCommandAuth")); +const { + parseConfiguredUrl, +} = require(path.join(__dirname, "../src/services/xCommand/xCommandChatCommand")); +const { + buildModuleManifest, +} = require(path.join(__dirname, "../src/services/xCommand/xCommandRegistry")); +const { + assignMinerToSite, + hireIndustrialContract, + performAction, + sanitizeCrewPackage, + sanitizeContract, +} = require(path.join(__dirname, "../src/services/xCommand/xCommandIndustrialAdapter")); +const { + listIndustrialMiningCrewPackages, +} = require(path.join( + __dirname, + "../src/services/industrialHirelings/industrialMiningCrewCatalog", +)); +const { + executeIndustrialHirelingCommand, +} = require(path.join( + __dirname, + "../src/services/industrialHirelings/industrialHirelingCommand", +)); + +function clone(value) { + return value === undefined ? undefined : JSON.parse(JSON.stringify(value)); +} + +function createMemoryRepo() { + const tables = {}; + function segments(pathArg) { + return String(pathArg || "/").split("/").filter(Boolean); + } + function read(table, pathArg) { + let value = tables[table]; + for (const segment of segments(pathArg)) { + value = value && typeof value === "object" ? value[segment] : undefined; + } + return { success: true, data: clone(value) }; + } + function write(table, pathArg, value) { + if (!tables[table]) tables[table] = {}; + const keys = segments(pathArg); + let target = tables[table]; + for (const key of keys.slice(0, -1)) { + if (!target[key] || typeof target[key] !== "object") target[key] = {}; + target = target[key]; + } + target[keys[keys.length - 1]] = clone(value); + return { success: true, data: clone(value) }; + } + function remove(table, pathArg) { + const keys = segments(pathArg); + let target = tables[table]; + for (const key of keys.slice(0, -1)) { + if (!target || typeof target !== "object") return { success: true }; + target = target[key]; + } + if (target && typeof target === "object") delete target[keys[keys.length - 1]]; + return { success: true }; + } + return { read, remove, write }; +} + +function verifyPersistentCharacterLinks() { + const nowMs = 1_000_000; + const auth = createAuthStore({ + repo: createMemoryRepo(), + config: { xCommandSessionHours: 1 }, + }); + const first = auth.issueForCharacter({ characterID: 9001, characterName: "Test Pilot" }, nowMs); + assert.strictEqual(first.success, true); + assert.ok(parseToken(first.data.token)); + assert.strictEqual(auth.verify(first.data.token, nowMs + 1000).success, true); + assert.strictEqual(auth.getCharacterStatus(9001, nowMs + 1000).linked, true); + + const second = auth.issueForCharacter({ characterID: 9001, characterName: "Test Pilot" }, nowMs + 2000); + assert.strictEqual(second.success, true); + assert.notStrictEqual(second.data.token, first.data.token); + assert.strictEqual(auth.verify(first.data.token, nowMs + 3000).success, false); + assert.strictEqual(auth.verify(second.data.token, nowMs + 3000).success, true); + assert.strictEqual(auth.verify(`${second.data.token}x`, nowMs + 3000).success, false); + assert.strictEqual(auth.verify(second.data.token, nowMs + 3_700_000).errorMsg, "X_COMMAND_TOKEN_EXPIRED"); +} + +function verifyUrlPolicy() { + const canonical = parseConfiguredUrl("https://COMMAND.example:443/", "ui"); + assert.strictEqual(canonical.success, true); + assert.strictEqual(canonical.url.toString(), "https://command.example/"); + assert.strictEqual(parseConfiguredUrl("http://127.0.0.1:26500/", "api").success, true); + assert.strictEqual(parseConfiguredUrl("http://command.example/", "ui").success, false); + assert.strictEqual(parseConfiguredUrl("https://user:pass@command.example/", "ui").success, false); + assert.strictEqual(parseConfiguredUrl("https://command.example/app", "ui").success, false); + assert.strictEqual(parseConfiguredUrl("https://command.example/?mode=map", "ui").success, false); + assert.strictEqual(parseConfiguredUrl("https://command.example/#map", "ui").success, false); + assert.strictEqual(parseConfiguredUrl("", "ui").success, false); +} + +function verifyModuleGates() { + const connected = buildModuleManifest({ + xCommandEnabled: true, + xCommandIndustrialEnabled: true, + livingUniverseEnabled: true, + industrialHirelingsEnabled: true, + }); + const industrial = connected.find((entry) => entry.id === "industrial"); + const logistics = connected.find((entry) => entry.id === "logistics"); + const universe = connected.find((entry) => entry.id === "universe"); + const mercenaries = connected.find((entry) => entry.id === "mercenaries"); + assert.strictEqual(industrial.state, "connected"); + assert.strictEqual(logistics.state, "partial"); + assert.strictEqual(universe.state, "connected"); + assert.strictEqual(universe.capability, "map_read"); + assert.strictEqual(mercenaries.state, "planned"); + + const disabled = buildModuleManifest({ xCommandEnabled: false }); + assert.strictEqual(disabled.find((entry) => entry.id === "industrial").enabled, false); +} + +function verifyContractSanitizer() { + const safe = sanitizeContract({ + contractID: "industrial-hireling-00000001", + serial: 1, + role: "miner", + state: "active", + order: "support_mining", + pilotName: "Sela Tori", + shipName: "Quiet Resolve", + shipTypeID: 17478, + tokenHash: "must-not-leak", + }); + assert.strictEqual(safe.shipTypeID, 17478); + assert.strictEqual(Object.prototype.hasOwnProperty.call(safe, "tokenHash"), false); +} + +function makeIndustrialConfig(overrides = {}) { + return { + xCommandIndustrialEnabled: true, + livingUniverseEnabled: true, + industrialHirelingsEnabled: true, + industrialHirelingsRemoteSitesEnabled: true, + industrialHirelingsOperationBookmarksEnabled: false, + industrialMiningCrewsEnabled: true, + industrialMiningCrewsLegacyHiringEnabled: false, + industrialMiningCrewsEconomyEnabled: false, + ...overrides, + }; +} + +function makeSiteCatalog(securityStatus = 0.9) { + return { + describeSite() { + return null; + }, + describeSystem() { + return null; + }, + getSystemIDForLocation() { + return 0; + }, + surveySystems() { + return []; + }, + validateSite(siteID) { + return { + success: true, + data: { + siteDescription: { + siteID, + siteName: `Test Belt ${siteID}`, + systemID: 30000142, + }, + systemDescription: { + systemID: 30000142, + systemName: "Test System", + securityStatus, + securityClass: securityStatus >= 0.5 ? "highsec" : "lowsec", + }, + }, + }; + }, + }; +} + +function makeCrewContract(overrides = {}) { + return { + schemaVersion: 2, + contractKind: "mining_crew", + contractID: "industrial-hireling-00000017", + serial: 17, + ownerCharacterID: 9001, + role: "miner", + state: "active", + order: "support_mining", + crewTypeID: "barge_crew", + tier: 2, + tierLabel: "II", + crewName: "Barge Crew #0017", + packageName: "Barge Crew", + composition: "3 Retrievers, 1 Porpoise, 1 Miasmos ore hauler", + homeStationID: 60003760, + destinationStationID: 60003760, + runtimeFleetID: 71, + runtimeEntityIDs: [101, 102, 103, 104, 105], + minerEntityIDs: [101, 102, 103], + supportEntityIDs: [104], + haulerEntityIDs: [105], + members: [ + { + memberID: "industrial-hireling-00000017:miner-01", + slotID: "miner-01", + role: "miner", + isCrewChief: false, + pilotName: "Miner One", + pilotActorID: "must-not-leak", + shipName: "Measured Yield", + shipTypeID: 17478, + shipTypeName: "Retriever", + profileID: "must-not-leak", + state: "materialized", + runtimeEntityID: 101, + }, + { + memberID: "industrial-hireling-00000017:support-01", + slotID: "support-01", + role: "mining_support", + isCrewChief: true, + pilotName: "Crew Chief", + shipName: "Deep Chorus", + shipTypeID: 42244, + shipTypeName: "Porpoise", + state: "materialized", + runtimeEntityID: 104, + }, + ], + expiresAtMs: 2_000_000, + updatedAtMs: 1_000_000, + ...overrides, + }; +} + +function verifyCrewPayloadSanitizers() { + const catalog = listIndustrialMiningCrewPackages().map(sanitizeCrewPackage); + assert.strictEqual(catalog.length, 6); + assert.strictEqual(catalog[0].crewTypeID, "venture_crew"); + assert.strictEqual(catalog[0].hullCount, 4); + assert.strictEqual(catalog[1].memberSlots.some((member) => member.shipTypeName === "Porpoise"), true); + assert.strictEqual( + Object.prototype.hasOwnProperty.call(catalog[0].memberSlots[0], "profileID"), + false, + ); + assert.strictEqual( + Object.prototype.hasOwnProperty.call(catalog[0].memberSlots[0], "loadoutID"), + false, + ); + + const safe = sanitizeContract(makeCrewContract(), { + siteCatalog: makeSiteCatalog(), + }); + assert.strictEqual(safe.contractKind, "mining_crew"); + assert.strictEqual(safe.crewTypeID, "barge_crew"); + assert.strictEqual(safe.crewTierLabel, "II"); + assert.strictEqual(safe.hullCount, 5); + assert.strictEqual(safe.deployedCount, 5); + assert.deepStrictEqual(safe.runtimeCounts, { + total: 5, + miners: 3, + miningSupport: 1, + haulers: 1, + expectedTotal: 5, + }); + assert.strictEqual(safe.members.length, 2); + assert.strictEqual(Object.prototype.hasOwnProperty.call(safe.members[0], "pilotActorID"), false); + assert.strictEqual(Object.prototype.hasOwnProperty.call(safe.members[0], "profileID"), false); +} + +function verifyCrewHireAction() { + const session = { + characterID: 9001, + corporationID: 1000123, + allianceID: 99001234, + warFactionID: 500001, + stationid: 60003760, + _space: { systemID: 30000142 }, + }; + const contracts = []; + let hiredInput = null; + const contractService = { + hire(input) { + hiredInput = clone(input); + const contract = makeCrewContract({ + runtimeFleetID: 0, + runtimeEntityIDs: [], + minerEntityIDs: [], + supportEntityIDs: [], + haulerEntityIDs: [], + members: makeCrewContract().members.map((member) => ({ + ...member, + state: "unmaterialized", + runtimeEntityID: 0, + })), + }); + contracts.push(contract); + return { success: true, data: clone(contract) }; + }, + listForCharacter() { + return { success: true, data: { enabled: true, contracts: clone(contracts) } }; + }, + }; + const result = performAction( + 9001, + "hire", + { + crewTypeID: "barge_crew", + commandID: "xc-hire-bridge-1", + }, + { + config: makeIndustrialConfig(), + session, + contractService, + siteCatalog: makeSiteCatalog(), + reconcileRuntime: false, + }, + ); + assert.strictEqual(result.success, true); + assert.deepStrictEqual(hiredInput, { + ownerCharacterID: 9001, + ownerCorporationID: 1000123, + ownerAllianceID: 99001234, + ownerWarFactionID: 500001, + homeStationID: 60003760, + currentSystemID: 30000142, + commandID: "xc-hire-bridge-1", + crewTypeID: "barge_crew", + }); + assert.strictEqual(result.data.crewModeEnabled, true); + assert.strictEqual(result.data.economyEnabled, false); + assert.strictEqual(result.data.crewPackages.length, 6); + assert.strictEqual(result.data.contracts[0].crewTypeID, "barge_crew"); +} + +function verifyCrewHireReplayMessage() { + const contract = makeCrewContract({ + runtimeFleetID: 0, + runtimeEntityIDs: [], + minerEntityIDs: [], + supportEntityIDs: [], + haulerEntityIDs: [], + }); + let hiredInput = null; + const result = hireIndustrialContract( + 9001, + { + crewTypeID: "venture_crew", + commandID: "xc-hire-replay-message-1", + }, + { + config: makeIndustrialConfig(), + session: { + characterID: 9001, + corporationID: 1000123, + stationid: 60003760, + }, + contractService: { + hire(input) { + hiredInput = clone(input); + return { + success: true, + replayed: true, + data: clone(contract), + }; + }, + listForCharacter() { + return { + success: true, + data: { + enabled: true, + contracts: [clone(contract)], + }, + }; + }, + }, + siteCatalog: makeSiteCatalog(), + reconcileRuntime: false, + }, + ); + + assert.strictEqual(result.success, true); + assert.strictEqual(hiredInput.commandID, "xc-hire-replay-message-1"); + assert.match(result.message, /already processed/); + assert.match(result.message, /no duplicate contract/); +} + +function verifyCrewSecurityRestriction() { + let standDownCount = 0; + let assignmentCount = 0; + const restrictedContract = makeCrewContract({ + crewTypeID: "rorqual_industrial_division", + tier: 6, + tierLabel: "VI", + crewName: "Rorqual Industrial Division #0017", + }); + const result = assignMinerToSite( + 9001, + restrictedContract.contractID, + { siteID: 401 }, + { + config: makeIndustrialConfig(), + session: { characterID: 9001, _space: { systemID: 30000142 } }, + siteCatalog: makeSiteCatalog(0.9), + contractService: { + listForCharacter() { + return { + success: true, + data: { enabled: true, contracts: [clone(restrictedContract)] }, + }; + }, + assignSite() { + assignmentCount += 1; + return { success: false, errorMsg: "unexpected" }; + }, + }, + hirelingRuntime: { + standDown() { + standDownCount += 1; + return { success: true }; + }, + }, + }, + ); + assert.strictEqual(result.success, false); + assert.strictEqual(result.errorMsg, "X_COMMAND_CREW_SECURITY_RESTRICTED"); + assert.strictEqual(standDownCount, 0); + assert.strictEqual(assignmentCount, 0); +} + +function verifyLegacyFallbackAndRejection() { + const session = { characterID: 9001, stationid: 60003760 }; + let legacyHireCount = 0; + const legacyContract = { + contractID: "industrial-hireling-00000001", + serial: 1, + ownerCharacterID: 9001, + role: "miner", + state: "active", + order: "standby", + homeStationID: 60003760, + destinationStationID: 60003760, + }; + const service = { + hire(input) { + legacyHireCount += 1; + assert.strictEqual(input.role, "miner"); + return { success: true, data: clone(legacyContract) }; + }, + listForCharacter() { + return { success: true, data: { enabled: true, contracts: [clone(legacyContract)] } }; + }, + }; + const allowed = hireIndustrialContract( + 9001, + { role: "miner" }, + { + config: makeIndustrialConfig({ + industrialMiningCrewsLegacyHiringEnabled: true, + }), + session, + contractService: service, + siteCatalog: makeSiteCatalog(), + reconcileRuntime: false, + }, + ); + assert.strictEqual(allowed.success, true); + assert.strictEqual(legacyHireCount, 1); + + const rejected = hireIndustrialContract( + 9001, + { role: "miner" }, + { + config: makeIndustrialConfig({ + industrialMiningCrewsLegacyHiringEnabled: false, + }), + session, + contractService: service, + }, + ); + assert.strictEqual(rejected.success, false); + assert.strictEqual(rejected.errorMsg, "X_COMMAND_LEGACY_HIRING_DISABLED"); + assert.strictEqual(legacyHireCount, 1); +} + +function verifyCrewChatCommand() { + let hiredInput = null; + const service = { + getMiningCrewFeatureState() { + return { crewModeEnabled: true, enabled: true }; + }, + hire(input) { + hiredInput = clone(input); + return { + success: true, + data: makeCrewContract({ + homeStationID: input.homeStationID, + runtimeFleetID: 0, + runtimeEntityIDs: [], + }), + }; + }, + }; + const session = { + characterID: 9001, + corporationID: 1000123, + stationid: 60003760, + }; + const config = makeIndustrialConfig(); + const hired = executeIndustrialHirelingCommand( + session, + "hire crew barge_crew", + { + config, + contractService: service, + hirelingRuntime: {}, + nowMs: 1_000_000, + }, + ); + assert.strictEqual(hired.success, true); + assert.strictEqual(hiredInput.crewTypeID, "barge_crew"); + assert.match(hired.message, /3 Retrievers, 1 Porpoise, 1 Miasmos/); + + const rejected = executeIndustrialHirelingCommand( + session, + "hire miner", + { + config, + contractService: service, + hirelingRuntime: {}, + nowMs: 1_000_000, + }, + ); + assert.strictEqual(rejected.success, false); + assert.match(rejected.message, /Individual pilot hiring is disabled/); +} + +verifyPersistentCharacterLinks(); +verifyUrlPolicy(); +verifyModuleGates(); +verifyContractSanitizer(); +verifyCrewPayloadSanitizers(); +verifyCrewHireAction(); +verifyCrewHireReplayMessage(); +verifyCrewSecurityRestriction(); +verifyLegacyFallbackAndRejection(); +verifyCrewChatCommand(); + +console.log("X Command bridge verification passed."); diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandLocalUi.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandLocalUi.js new file mode 100644 index 0000000..eff34f5 --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandLocalUi.js @@ -0,0 +1,369 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("fs"); +const path = require("path"); + +const { + createAuthStore, + parseToken, +} = require(path.join(__dirname, "../src/services/xCommand/xCommandAuth")); +const { + executeXCommandChatCommand, + getDefaultLocalUrl, +} = require(path.join( + __dirname, + "../src/services/xCommand/xCommandChatCommand", +)); +const { + buildExpiredSessionCookie, + buildSessionCookie, + getRequestToken, + normalizeHttpOrigin, + revokeRequestSession, + setCorsHeaders, +} = require(path.join(__dirname, "../src/_secondary/xCommand/httpAuth")); +const { + DEFAULT_UI_ROOT, + UI_SECURITY_HEADERS, + isWithinRoot, + resolveUiAssetPath, +} = require(path.join(__dirname, "../src/_secondary/xCommand/staticUi")); + +function clone(value) { + return value === undefined ? undefined : JSON.parse(JSON.stringify(value)); +} + +function createMemoryRepo() { + const tables = {}; + const failures = []; + const segments = (value) => String(value || "/").split("/").filter(Boolean); + function shouldFail(pathArg) { + const index = failures.findIndex((prefix) => + String(pathArg || "").startsWith(prefix)); + if (index < 0) return false; + failures.splice(index, 1); + return true; + } + return { + failNextWrite(prefix) { + failures.push(String(prefix)); + }, + read(table, pathArg) { + let value = tables[table]; + for (const segment of segments(pathArg)) { + value = value && typeof value === "object" ? value[segment] : undefined; + } + return { success: true, data: clone(value) }; + }, + write(table, pathArg, value) { + if (shouldFail(pathArg)) { + return { success: false, errorMsg: "TEST_WRITE_FAILED" }; + } + if (!tables[table]) tables[table] = {}; + const parts = segments(pathArg); + let target = tables[table]; + for (const part of parts.slice(0, -1)) { + if (!target[part] || typeof target[part] !== "object") target[part] = {}; + target = target[part]; + } + target[parts[parts.length - 1]] = clone(value); + return { success: true }; + }, + remove(table, pathArg) { + const parts = segments(pathArg); + let target = tables[table]; + for (const part of parts.slice(0, -1)) target = target && target[part]; + if (target && typeof target === "object") { + delete target[parts[parts.length - 1]]; + } + return { success: true }; + }, + }; +} + +function verifyOneTimeExchange() { + const auth = createAuthStore({ + repo: createMemoryRepo(), + config: { + xCommandLinkMinutes: 5, + xCommandSessionHours: 1, + }, + }); + const existing = auth.issueForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 1000); + assert.equal(existing.success, true); + + const link = auth.issueLinkForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 2000); + assert.equal(link.success, true); + assert.equal(link.data.expiresAtMs - link.data.issuedAtMs, 5 * 60 * 1000); + assert.equal(auth.verify(existing.data.token, 3000).success, true); + assert.equal( + auth.verify(link.data.token, 3000).errorMsg, + "X_COMMAND_TOKEN_EXCHANGE_ONLY", + ); + + const session = auth.exchangeLink(link.data.token, 4000); + assert.equal(session.success, true); + assert.equal( + session.data.expiresAtMs - session.data.issuedAtMs, + 60 * 60 * 1000, + ); + assert.equal(auth.exchangeLink(link.data.token, 5000).success, false); + assert.equal(auth.verify(link.data.token, 5000).success, false); + assert.equal(auth.verify(existing.data.token, 5000).success, false); + assert.equal(auth.verify(session.data.token, 5000).success, true); +} + +function verifySafeReplacementFailures() { + const repo = createMemoryRepo(); + const auth = createAuthStore({ + repo, + config: { + xCommandLinkMinutes: 5, + xCommandSessionHours: 1, + }, + }); + let active = auth.issueForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 1000); + assert.equal(active.success, true); + + repo.failNextWrite("/links/"); + const failedLink = auth.issueLinkForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 2000); + assert.equal(failedLink.success, false); + assert.equal(auth.verify(active.data.token, 2500).success, true); + + let link = auth.issueLinkForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 3000); + assert.equal(link.success, true); + repo.failNextWrite("/sessions/"); + assert.equal(auth.exchangeLink(link.data.token, 4000).success, false); + assert.equal(auth.verify(active.data.token, 4500).success, true); + const firstExchange = auth.exchangeLink(link.data.token, 5000); + assert.equal(firstExchange.success, true); + active = firstExchange; + + link = auth.issueLinkForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 6000); + assert.equal(link.success, true); + repo.failNextWrite("/characters/9001"); + assert.equal(auth.exchangeLink(link.data.token, 7000).success, false); + assert.equal(auth.verify(active.data.token, 7500).success, true); + const retried = auth.exchangeLink(link.data.token, 8000); + assert.equal(retried.success, true); + assert.equal(auth.verify(active.data.token, 8500).success, false); + assert.equal(auth.verify(retried.data.token, 8500).success, true); + + repo.failNextWrite("/characters/9001"); + const failedDirectSession = auth.issueForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 9000); + assert.equal(failedDirectSession.success, false); + assert.equal(auth.verify(retried.data.token, 9500).success, true); +} + +function verifyLinkLifetimeClamp() { + const auth = createAuthStore({ + repo: createMemoryRepo(), + config: { + xCommandLinkMinutes: 999, + xCommandSessionHours: 1, + }, + }); + const link = auth.issueLinkForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 1000); + assert.equal(link.success, true); + assert.equal(link.data.expiresAtMs - link.data.issuedAtMs, 30 * 60 * 1000); +} + +function verifyLegacyBearerFailsClosed() { + const repo = createMemoryRepo(); + const auth = createAuthStore({ + repo, + config: { xCommandSessionHours: 1 }, + }); + const session = auth.issueForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, 1000); + const parsed = parseToken(session.data.token); + const stored = repo.read( + "xCommandAuth", + `/sessions/${parsed.tokenID}`, + ).data; + delete stored.kind; + repo.write("xCommandAuth", `/sessions/${parsed.tokenID}`, stored); + assert.equal(auth.verify(session.data.token, 2000).success, false); + assert.equal(auth.getCharacterStatus(9001, 2000).linked, false); +} + +function verifyLocalLink() { + const sourceConfig = { + xCommandEnabled: true, + xCommandApiBindHost: "127.0.0.1", + xCommandApiPort: 26500, + xCommandPublicUrl: "", + xCommandApiPublicUrl: "", + }; + assert.equal(getDefaultLocalUrl(sourceConfig), "http://127.0.0.1:26500/"); + const result = executeXCommandChatCommand( + { characterID: 9001, characterName: "Map Pilot" }, + "open", + { + config: sourceConfig, + authStore: { + issueLinkForCharacter() { + return { + success: true, + data: { characterName: "Map Pilot", token: "one-time-link" }, + }; + }, + }, + }, + ); + assert.equal(result.success, true); + assert.match(result.message, /http:\/\/127\.0\.0\.1:26500\/#xc_api=/); + assert.match(result.message, /xc_token=one-time-link/); + assert.doesNotMatch(result.message, /\?xc_token=/); +} + +function verifyOriginPolicy() { + function responseRecorder() { + const headers = {}; + return { + headers, + setHeader(name, value) { + headers[String(name).toLowerCase()] = value; + }, + }; + } + assert.equal(normalizeHttpOrigin("null"), ""); + assert.equal(normalizeHttpOrigin("https://command.example/path"), ""); + assert.equal(normalizeHttpOrigin("https://command.example/"), "https://command.example"); + + let res = responseRecorder(); + assert.equal(setCorsHeaders({ headers: { host: "127.0.0.1:26500" } }, res), true); + res = responseRecorder(); + assert.equal(setCorsHeaders({ + headers: { + host: "127.0.0.1:26500", + origin: "null", + }, + }, res), false); + res = responseRecorder(); + assert.equal(setCorsHeaders({ + headers: { + host: "127.0.0.1:26500", + origin: "http://127.0.0.1:26500", + }, + }, res), true); + assert.equal( + res.headers["access-control-allow-origin"], + "http://127.0.0.1:26500", + ); + res = responseRecorder(); + assert.equal(setCorsHeaders({ + headers: { + host: "127.0.0.1:26500", + origin: "https://command.example", + }, + }, res, { + xCommandAllowedOrigins: "https://command.example", + }), true); + res = responseRecorder(); + assert.equal(setCorsHeaders({ + headers: { + host: "127.0.0.1:26500", + origin: "https://untrusted.example", + }, + }, res, { + xCommandAllowedOrigins: "https://command.example", + }), false); +} + +function verifyCookieLogoutAndStaticPaths() { + assert.equal( + getRequestToken({ headers: { cookie: "other=1; xc_session=cookie-token" } }), + "cookie-token", + ); + const request = { headers: { "x-forwarded-proto": "https" } }; + const cookie = buildSessionCookie( + "session-token", + Date.now() + 60_000, + request, + ); + assert.match(cookie, /HttpOnly/); + assert.match(cookie, /SameSite=Strict/); + assert.match(cookie, /Secure/); + assert.match(buildExpiredSessionCookie(request), /Max-Age=0/); + + const auth = createAuthStore({ + repo: createMemoryRepo(), + config: { xCommandSessionHours: 1 }, + }); + const nowMs = Date.now(); + const session = auth.issueForCharacter({ + characterID: 9001, + characterName: "Map Pilot", + }, nowMs); + const logout = revokeRequestSession(auth, { + headers: { cookie: `xc_session=${encodeURIComponent(session.data.token)}` }, + }); + assert.equal(logout.success, true); + assert.equal(logout.data.revoked, true); + assert.equal(auth.verify(session.data.token, nowMs + 1000).success, false); + assert.equal( + revokeRequestSession(auth, { headers: {} }).data.revoked, + false, + ); + + const indexPath = resolveUiAssetPath(DEFAULT_UI_ROOT, "/"); + assert.equal(indexPath, path.join(DEFAULT_UI_ROOT, "index.html")); + assert.equal( + isWithinRoot(DEFAULT_UI_ROOT, path.resolve(DEFAULT_UI_ROOT, "..", "secret")), + false, + ); + assert.equal(resolveUiAssetPath(DEFAULT_UI_ROOT, "/../../secret"), null); + assert.match( + UI_SECURITY_HEADERS["content-security-policy"], + /frame-ancestors 'none'/, + ); + assert.match( + UI_SECURITY_HEADERS["content-security-policy"], + /https:\/\/images\.evetech\.net/, + ); + assert.equal(UI_SECURITY_HEADERS["x-frame-options"], "DENY"); +} + +function verifyPackagedUiWhenPresent() { + if (!fs.existsSync(DEFAULT_UI_ROOT)) return; + assert.equal(fs.existsSync(path.join(DEFAULT_UI_ROOT, "index.html")), true); +} + +verifyOneTimeExchange(); +verifySafeReplacementFailures(); +verifyLinkLifetimeClamp(); +verifyLegacyBearerFailsClosed(); +verifyLocalLink(); +verifyOriginPolicy(); +verifyCookieLogoutAndStaticPaths(); +verifyPackagedUiWhenPresent(); + +console.log("X Command local UI verification passed."); diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandUniverse.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandUniverse.js new file mode 100644 index 0000000..9bb85dc --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/scripts/verifyXCommandUniverse.js @@ -0,0 +1,491 @@ +"use strict"; + +const assert = require("assert"); +const path = require("path"); + +const { + createUniverseAdapter, +} = require(path.join( + __dirname, + "../src/services/xCommand/xCommandUniverseAdapter", +)); +const { + __testHooks: { + handleRequest, + }, +} = require(path.join(__dirname, "../src/_secondary/xCommand/server")); +const sessionRegistry = require(path.join( + __dirname, + "../src/services/chat/sessionRegistry", +)); + +function makeWorldData() { + const counters = { + getSolarSystems: 0, + getStations: 0, + getStargatesForSystem: 0, + }; + const systems = [ + { + solarSystemID: 10, + solarSystemName: "Alpha", + constellationID: 100, + regionID: 1000, + factionID: 500001, + security: 0.9, + securityClass: "A", + position: { x: 1, y: 2, z: 3 }, + internalSecret: "must-not-leak", + }, + { + solarSystemID: 20, + solarSystemName: "Beta", + constellationID: 100, + regionID: 1000, + factionID: 500001, + security: 0.5, + securityClass: "B", + position: { x: 4, y: 5, z: 6 }, + }, + { + solarSystemID: 30, + solarSystemName: "Gamma", + constellationID: 200, + regionID: 2000, + security: -0.2, + securityClass: "F", + position: { x: 7, y: 8, z: 9 }, + }, + ]; + const systemsByID = new Map( + systems.map((system) => [system.solarSystemID, system]), + ); + const gatesBySystem = new Map([ + [10, [{ + itemID: 1001, + destinationID: 2001, + destinationSolarSystemID: 20, + hiddenRuntimeState: "must-not-leak", + }]], + [20, [{ + itemID: 2001, + destinationID: 1001, + destinationSolarSystemID: 10, + }]], + [30, []], + ]); + const stations = [{ + stationID: 6001, + stationName: "Alpha Trade Hub", + solarSystemID: 10, + constellationID: 100, + regionID: 1000, + stationTypeID: 1531, + corporationID: 999, + dockingSecret: "must-not-leak", + }]; + return { + counters, + getSolarSystemByID(solarSystemID) { + return systemsByID.get(Number(solarSystemID)) || null; + }, + getSolarSystems() { + counters.getSolarSystems += 1; + return systems; + }, + getStargatesForSystem(solarSystemID) { + counters.getStargatesForSystem += 1; + return gatesBySystem.get(Number(solarSystemID)) || []; + }, + getStations() { + counters.getStations += 1; + return stations; + }, + }; +} + +function makeAdapter(overrides = {}) { + const sourceWorldData = overrides.worldData || makeWorldData(); + const listCalls = []; + const contractService = overrides.contractService || { + listForCharacter(characterID) { + listCalls.push(characterID); + return { + success: true, + data: { + enabled: true, + contracts: [ + { + contractID: "owned-contract", + ownerCharacterID: characterID, + contractKind: "mining_crew", + role: "miner", + state: "active", + order: "standby", + crewName: "Owned Crew", + assignedSystemID: 10, + tokenHash: "must-not-leak", + }, + { + contractID: "foreign-contract", + ownerCharacterID: characterID + 1, + contractKind: "mining_crew", + role: "miner", + state: "active", + order: "standby", + crewName: "Foreign Crew", + }, + ], + }, + }; + }, + }; + const adapter = createUniverseAdapter({ + worldData: sourceWorldData, + marketTopology: overrides.marketTopology || { + getShortestPath(fromSolarSystemID, toSolarSystemID) { + if (fromSolarSystemID === 10 && toSolarSystemID === 20) { + return [10, 20]; + } + return fromSolarSystemID === toSolarSystemID + ? [fromSolarSystemID] + : []; + }, + }, + mapTelemetryState: overrides.mapTelemetryState || { + listSolarSystemVisits(characterID) { + assert.strictEqual(characterID, 9001); + return [{ + lastDateTime: "123", + lastDateTimeMs: 777, + solarSystemID: 10, + visits: 3, + }]; + }, + listSolarSystemVisitRows() { + throw new Error("Snapshot must use the ownership-scoped visit reader."); + }, + msFromFiletime() { + throw new Error("Stored visit milliseconds should not need conversion."); + }, + }, + findSessionByCharacterID: overrides.findSessionByCharacterID || ((characterID) => ({ + characterID, + characterName: "Test Pilot", + stationid: 6001, + _space: { + systemID: 10, + shipID: 7001, + }, + })), + contractService, + siteCatalog: overrides.siteCatalog || { + describeSite() { + return null; + }, + describeSystem(systemID) { + return { + systemID, + systemName: sourceWorldData.getSolarSystemByID(systemID)?.solarSystemName || "", + }; + }, + }, + now: () => 1000, + }); + return { adapter, listCalls, worldData: sourceWorldData }; +} + +function verifyTopologyIsSanitizedCachedAndImmutable() { + const { adapter, worldData } = makeAdapter(); + const first = adapter.getTopology(); + const second = adapter.getTopology(); + assert.strictEqual(first.success, true); + assert.strictEqual(first.data, second.data); + assert.strictEqual(worldData.counters.getSolarSystems, 1); + assert.strictEqual(worldData.counters.getStations, 1); + assert.strictEqual(worldData.counters.getStargatesForSystem, 3); + assert.deepStrictEqual(first.data.counts, { + systems: 3, + connectedSystems: 2, + connections: 1, + stations: 1, + }); + assert.deepStrictEqual(first.data.connections[0], { + fromSolarSystemID: 10, + toSolarSystemID: 20, + fromStargateID: 1001, + toStargateID: 2001, + }); + assert.strictEqual(first.data.systems[0].hasStargateConnection, true); + assert.strictEqual(first.data.systems[2].hasStargateConnection, false); + assert.strictEqual( + Object.prototype.hasOwnProperty.call(first.data.systems[0], "internalSecret"), + false, + ); + assert.deepStrictEqual(first.data.stations[0], { + stationID: 6001, + stationName: "Alpha Trade Hub", + solarSystemID: 10, + constellationID: 100, + regionID: 1000, + stationTypeID: 1531, + }); + assert.strictEqual(Object.isFrozen(first.data), true); + assert.strictEqual(Object.isFrozen(first.data.systems), true); + assert.strictEqual(Object.isFrozen(first.data.systems[0].position), true); + assert.match(first.data.topologyVersion, /^[a-f0-9]{16}$/); +} + +function verifyShortestRouteResponses() { + const { adapter } = makeAdapter(); + const route = adapter.getRoute(10, 20); + assert.strictEqual(route.success, true); + assert.strictEqual(route.data.reachable, true); + assert.strictEqual(route.data.jumps, 1); + assert.deepStrictEqual(route.data.systemIDs, [10, 20]); + assert.strictEqual(route.data.systems[1].solarSystemName, "Beta"); + + const unreachable = adapter.getRoute(10, 30); + assert.strictEqual(unreachable.success, true); + assert.strictEqual(unreachable.data.reachable, false); + assert.strictEqual(unreachable.data.jumps, null); + assert.deepStrictEqual(unreachable.data.systemIDs, []); + + assert.strictEqual(adapter.getRoute(0, 20).success, false); + assert.strictEqual( + adapter.getRoute(10, 999).errorMsg, + "X_COMMAND_UNIVERSE_SYSTEM_NOT_FOUND", + ); +} + +function verifySnapshotIsCharacterScopedAndPureRead() { + let sessionLookupCharacterID = 0; + const sourceWorldData = makeWorldData(); + const { adapter, listCalls } = makeAdapter({ + worldData: sourceWorldData, + findSessionByCharacterID(characterID, lookupOptions) { + sessionLookupCharacterID = characterID; + assert.deepStrictEqual(lookupOptions, { indexedOnly: true }); + return { + characterID, + characterName: "Test Pilot", + stationid: 6001, + _space: { systemID: 10, shipID: 7001 }, + }; + }, + }); + const result = adapter.getSnapshot(9001, { + authRecord: { + characterID: 9001, + characterName: "Fallback Name", + tokenHash: "must-not-leak", + }, + }); + assert.strictEqual(result.success, true); + assert.strictEqual(sessionLookupCharacterID, 9001); + assert.deepStrictEqual(listCalls, [9001]); + assert.strictEqual(sourceWorldData.counters.getSolarSystems, 0); + assert.strictEqual(sourceWorldData.counters.getStations, 0); + assert.strictEqual(sourceWorldData.counters.getStargatesForSystem, 0); + assert.deepStrictEqual(result.data.character, { + characterID: 9001, + characterName: "Test Pilot", + online: true, + docked: true, + dockedLocationID: 6001, + solarSystemID: 10, + shipID: 7001, + solarSystemName: "Alpha", + }); + assert.deepStrictEqual(result.data.visits, [{ + solarSystemID: 10, + solarSystemName: "Alpha", + visits: 3, + lastVisitedAtMs: 777, + }]); + assert.strictEqual(result.data.industrial.contracts.length, 1); + assert.strictEqual( + result.data.industrial.contracts[0].contractID, + "owned-contract", + ); + assert.strictEqual( + Object.prototype.hasOwnProperty.call( + result.data.industrial.contracts[0], + "tokenHash", + ), + false, + ); + assert.strictEqual(result.data.serverTimeMs, 1000); +} + +function verifyIndexedSessionLookupAvoidsFallbackScan() { + const unindexed = { + socket: { destroyed: false }, + characterID: 0, + }; + sessionRegistry.register(unindexed); + unindexed.characterID = 99117; + try { + assert.strictEqual( + sessionRegistry.findSessionByCharacterID(99117), + unindexed, + ); + assert.strictEqual( + sessionRegistry.findSessionByCharacterID(99117, { indexedOnly: true }), + null, + ); + } finally { + sessionRegistry.unregister(unindexed); + } +} + +function createResponseRecorder() { + return { + headers: {}, + statusCode: 0, + body: "", + setHeader(name, value) { + this.headers[String(name).toLowerCase()] = value; + }, + writeHead(statusCode, headers = {}) { + this.statusCode = statusCode; + for (const [name, value] of Object.entries(headers)) { + this.headers[String(name).toLowerCase()] = value; + } + }, + end(body = "") { + this.body = String(body || ""); + }, + }; +} + +async function requestUniverse(url, universeAdapter, headers = {}) { + const req = { + method: "GET", + url, + headers: { + authorization: "Bearer test", + ...headers, + }, + }; + const res = createResponseRecorder(); + await handleRequest(req, res, { + authStore: { + verify() { + return { + success: true, + data: { + tokenID: "test-token", + characterID: 9001, + characterName: "Test Pilot", + }, + }; + }, + }, + universeAdapter, + }); + return res; +} + +async function verifyHttpEndpoints() { + const calls = []; + const universeAdapter = { + getTopology() { + calls.push(["topology"]); + return { + success: true, + data: { + topologyVersion: "abc123", + systems: [], + connections: [], + stations: [], + }, + }; + }, + getRoute(fromSolarSystemID, toSolarSystemID) { + calls.push(["route", fromSolarSystemID, toSolarSystemID]); + return { + success: true, + data: { + fromSolarSystemID: Number(fromSolarSystemID), + toSolarSystemID: Number(toSolarSystemID), + reachable: true, + jumps: 1, + systemIDs: [10, 20], + }, + }; + }, + getSnapshot(characterID, context) { + calls.push([ + "snapshot", + characterID, + context && context.authRecord && context.authRecord.characterID, + ]); + return { + success: true, + data: { + character: { characterID }, + visits: [], + industrial: { contracts: [] }, + }, + }; + }, + }; + + const topology = await requestUniverse( + "/api/v1/universe/topology", + universeAdapter, + ); + assert.strictEqual(topology.statusCode, 200); + assert.strictEqual(topology.headers.etag, "\"xc-universe-abc123\""); + assert.strictEqual( + topology.headers["cache-control"], + "private, max-age=3600", + ); + assert.strictEqual(JSON.parse(topology.body).success, true); + + const unchanged = await requestUniverse( + "/api/v1/universe/topology", + universeAdapter, + { "if-none-match": "\"xc-universe-abc123\"" }, + ); + assert.strictEqual(unchanged.statusCode, 304); + assert.strictEqual(unchanged.body, ""); + + const route = await requestUniverse( + "/api/v1/universe/route?from=10&to=20", + universeAdapter, + ); + assert.strictEqual(route.statusCode, 200); + assert.deepStrictEqual(JSON.parse(route.body).data.systemIDs, [10, 20]); + + const snapshot = await requestUniverse( + "/api/v1/universe/snapshot", + universeAdapter, + ); + assert.strictEqual(snapshot.statusCode, 200); + assert.strictEqual( + JSON.parse(snapshot.body).data.character.characterID, + 9001, + ); + assert.deepStrictEqual(calls, [ + ["topology"], + ["topology"], + ["route", "10", "20"], + ["snapshot", 9001, 9001], + ]); +} + +async function main() { + verifyTopologyIsSanitizedCachedAndImmutable(); + verifyShortestRouteResponses(); + verifySnapshotIsCharacterScopedAndPureRead(); + verifyIndexedSessionLookupAvoidsFallbackScan(); + await verifyHttpEndpoints(); + console.log("X Command universe verification passed."); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/httpAuth.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/httpAuth.js new file mode 100644 index 0000000..917df67 --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/httpAuth.js @@ -0,0 +1,156 @@ +"use strict"; + +const COOKIE_NAME = "xc_session"; + +function parseCookies(headerValue) { + const cookies = {}; + for (const pair of String(headerValue || "").split(";")) { + const separator = pair.indexOf("="); + if (separator <= 0) continue; + const name = pair.slice(0, separator).trim(); + if (!name) continue; + try { + cookies[name] = decodeURIComponent(pair.slice(separator + 1).trim()); + } catch { + cookies[name] = ""; + } + } + return cookies; +} + +function getBearerToken(req) { + const match = String(req && req.headers && req.headers.authorization || "") + .match(/^Bearer\s+(.+)$/i); + return match ? match[1].trim() : ""; +} + +function getRequestToken(req) { + return getBearerToken(req) || + String(parseCookies(req && req.headers && req.headers.cookie)[COOKIE_NAME] || ""); +} + +function isSecureRequest(req) { + if (req && req.socket && req.socket.encrypted) return true; + return String( + req && req.headers && req.headers["x-forwarded-proto"] || "", + ).split(",")[0].trim().toLowerCase() === "https"; +} + +function buildSessionCookie(token, expiresAtMs, req) { + const expires = new Date(Math.max(Date.now(), Number(expiresAtMs) || 0)); + const attributes = [ + `${COOKIE_NAME}=${encodeURIComponent(String(token || ""))}`, + "Path=/", + "HttpOnly", + "SameSite=Strict", + `Expires=${expires.toUTCString()}`, + ]; + if (isSecureRequest(req)) attributes.push("Secure"); + return attributes.join("; "); +} + +function buildExpiredSessionCookie(req) { + const attributes = [ + `${COOKIE_NAME}=`, + "Path=/", + "HttpOnly", + "SameSite=Strict", + "Expires=Thu, 01 Jan 1970 00:00:00 GMT", + "Max-Age=0", + ]; + if (isSecureRequest(req)) attributes.push("Secure"); + return attributes.join("; "); +} + +function normalizeHttpOrigin(value) { + const raw = String(value || "").trim(); + if (!raw || raw.toLowerCase() === "null") return ""; + try { + const parsed = new URL(raw); + if (!["http:", "https:"].includes(parsed.protocol)) return ""; + if (parsed.username || parsed.password) return ""; + if (parsed.pathname !== "/" || parsed.search || parsed.hash) return ""; + return parsed.origin; + } catch { + return ""; + } +} + +function getAllowedOrigins(sourceConfig = {}) { + const origins = new Set(); + const configuredInterface = normalizeHttpOrigin( + sourceConfig.xCommandPublicUrl, + ); + if (configuredInterface) origins.add(configuredInterface); + String(sourceConfig.xCommandAllowedOrigins || "") + .split(",") + .map(normalizeHttpOrigin) + .filter(Boolean) + .forEach((origin) => origins.add(origin)); + return origins; +} + +function setCorsHeaders(req, res, sourceConfig = {}) { + const headers = req && req.headers || {}; + if (!Object.prototype.hasOwnProperty.call(headers, "origin")) return true; + + const origin = normalizeHttpOrigin(headers.origin); + if (!origin) return false; + const requestOrigin = normalizeHttpOrigin( + `${isSecureRequest(req) ? "https" : "http"}://${headers.host || ""}`, + ); + if ( + !requestOrigin || + origin !== requestOrigin && + !getAllowedOrigins(sourceConfig).has(origin) + ) { + return false; + } + res.setHeader("access-control-allow-origin", origin); + res.setHeader("access-control-allow-credentials", "true"); + res.setHeader("vary", "Origin"); + res.setHeader( + "access-control-allow-headers", + "authorization,content-type,if-none-match", + ); + res.setHeader("access-control-allow-methods", "GET,POST,OPTIONS"); + res.setHeader("access-control-max-age", "600"); + return true; +} + +function revokeRequestSession(authStore, req) { + if (!authStore || typeof authStore.verify !== "function") { + return { success: false, errorMsg: "X_COMMAND_AUTH_UNAVAILABLE" }; + } + const verified = authStore.verify(getRequestToken(req)); + if (!verified || verified.success !== true) { + return { success: true, data: { revoked: false } }; + } + if (typeof authStore.revokeTokenID !== "function") { + return { success: false, errorMsg: "X_COMMAND_AUTH_UNAVAILABLE" }; + } + const revoked = authStore.revokeTokenID( + verified.data && verified.data.tokenID, + ); + if (!revoked || revoked.success !== true) { + return revoked || { + success: false, + errorMsg: "X_COMMAND_AUTH_REVOKE_FAILED", + }; + } + return { success: true, data: { revoked: true } }; +} + +module.exports = { + COOKIE_NAME, + buildExpiredSessionCookie, + buildSessionCookie, + getAllowedOrigins, + getBearerToken, + getRequestToken, + isSecureRequest, + normalizeHttpOrigin, + parseCookies, + revokeRequestSession, + setCorsHeaders, +}; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/server.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/server.js new file mode 100644 index 0000000..7ce749e --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/server.js @@ -0,0 +1,365 @@ +"use strict"; + +const http = require("http"); +const path = require("path"); + +const config = require(path.join(__dirname, "../../config")); +const log = require(path.join(__dirname, "../../utils/logger")); +const sessionRegistry = require(path.join(__dirname, "../../services/chat/sessionRegistry")); +const { getDefaultAuthStore } = require(path.join( + __dirname, + "../../services/xCommand/xCommandAuth", +)); +const { buildModuleManifest } = require(path.join( + __dirname, + "../../services/xCommand/xCommandRegistry", +)); +const industrialAdapter = require(path.join( + __dirname, + "../../services/xCommand/xCommandIndustrialAdapter", +)); +const { + getDefaultUniverseAdapter, +} = require(path.join( + __dirname, + "../../services/xCommand/xCommandUniverseAdapter", +)); +const { + buildExpiredSessionCookie, + buildSessionCookie, + getAllowedOrigins, + getRequestToken, + normalizeHttpOrigin, + revokeRequestSession, + setCorsHeaders, +} = require("./httpAuth"); +const { serveUiRequest } = require("./staticUi"); + +const MAX_BODY_BYTES = 32 * 1024; +const POST_WINDOW_MS = 5000; +const POST_LIMIT = 10; +const postWindows = new Map(); + +function toPositiveInt(value, fallback = 0) { + const numeric = Math.trunc(Number(value) || 0); + return numeric > 0 ? numeric : fallback; +} + +function sendJson(res, statusCode, payload, extraHeaders = {}) { + const body = JSON.stringify(payload); + res.writeHead(statusCode, { + "content-type": "application/json; charset=utf-8", + "content-length": Buffer.byteLength(body), + "cache-control": "no-store", + "x-content-type-options": "nosniff", + ...extraHeaders, + }); + res.end(body); +} + +function sendError(res, statusCode, code, message, extraHeaders = {}) { + sendJson(res, statusCode, { + success: false, + error: { code, message }, + }, extraHeaders); +} + +function readJsonBody(req) { + return new Promise((resolve, reject) => { + let size = 0; + let failed = false; + const chunks = []; + req.on("data", (chunk) => { + if (failed) return; + size += chunk.length; + if (size > MAX_BODY_BYTES) { + failed = true; + reject(Object.assign(new Error("Request body is too large."), { code: "BODY_TOO_LARGE" })); + return; + } + chunks.push(chunk); + }); + req.on("end", () => { + if (failed) return; + if (chunks.length === 0) { + resolve({}); + return; + } + try { + const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8")); + resolve(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}); + } catch { + reject(Object.assign(new Error("Request body is not valid JSON."), { code: "JSON_INVALID" })); + } + }); + req.on("error", reject); + }); +} + +function checkPostRateLimit(tokenID, nowMs = Date.now()) { + const key = String(tokenID || ""); + const now = Math.max(0, Math.trunc(Number(nowMs) || Date.now())); + let window = postWindows.get(key); + if (!window || now - window.startedAtMs >= POST_WINDOW_MS) { + window = { startedAtMs: now, count: 0 }; + postWindows.set(key, window); + } + window.count += 1; + return window.count <= POST_LIMIT; +} + +function buildCharacterSnapshot(authRecord) { + const characterID = toPositiveInt(authRecord && authRecord.characterID, 0); + const session = sessionRegistry.findSessionByCharacterID(characterID); + return { + characterID, + characterName: String( + session && (session.characterName || session.charname) || + authRecord && authRecord.characterName || + `Character ${characterID}`, + ), + ...industrialAdapter.getSessionLocation(session), + }; +} + +async function handleRequest(req, res, options = {}) { + if (!setCorsHeaders(req, res, options.config || config)) { + sendError(res, 403, "ORIGIN_NOT_ALLOWED", "This browser origin is not allowed to use X Command."); + return; + } + if (req.method === "OPTIONS") { + res.writeHead(204, { "cache-control": "no-store" }); + res.end(); + return; + } + const requestUrl = new URL(req.url || "/", "http://x-command.local"); + if (req.method === "GET" && requestUrl.pathname === "/health") { + sendJson(res, 200, { + success: true, + service: "x-command", + version: 1, + modules: buildModuleManifest(), + }); + return; + } + if (await serveUiRequest(req, res, requestUrl, options.staticUi)) { + return; + } + if (!requestUrl.pathname.startsWith("/api/v1/")) { + sendError(res, 404, "NOT_FOUND", "X Command endpoint not found."); + return; + } + + const authStore = options.authStore || getDefaultAuthStore(); + const universeAdapter = options.universeAdapter || getDefaultUniverseAdapter(); + if (req.method === "POST" && requestUrl.pathname === "/api/v1/auth/exchange") { + let body; + try { + body = await readJsonBody(req); + } catch (error) { + sendError( + res, + error && error.code === "BODY_TOO_LARGE" ? 413 : 400, + error.code || "BODY_INVALID", + error.message, + ); + return; + } + const exchanged = authStore.exchangeLink(body.token); + if (!exchanged || exchanged.success !== true) { + const expired = exchanged && exchanged.errorMsg === "X_COMMAND_TOKEN_EXPIRED"; + sendError( + res, + 401, + expired ? "TOKEN_EXPIRED" : "TOKEN_INVALID", + expired + ? "This character link expired. Use /x in game for a fresh link." + : "This character link is invalid. Use /x in game for a fresh link.", + ); + return; + } + const exchangeData = exchanged.data || {}; + sendJson(res, 200, { + success: true, + data: { + characterID: toPositiveInt(exchangeData.characterID, 0), + characterName: String(exchangeData.characterName || ""), + expiresAtMs: toPositiveInt(exchangeData.expiresAtMs, 0), + }, + }, { + "set-cookie": buildSessionCookie( + exchangeData.token, + exchangeData.expiresAtMs, + req, + ), + }); + return; + } + + if (req.method === "POST" && requestUrl.pathname === "/api/v1/auth/logout") { + const sessionCookie = buildExpiredSessionCookie(req); + const logoutResult = revokeRequestSession(authStore, req); + if (!logoutResult || logoutResult.success !== true) { + sendError( + res, + 500, + "LOGOUT_REVOKE_FAILED", + "The browser cookie was cleared, but EveJS could not revoke the server session.", + { "set-cookie": sessionCookie }, + ); + return; + } + sendJson(res, 200, { + success: true, + data: { + revoked: Boolean( + logoutResult.data && logoutResult.data.revoked, + ), + }, + }, { + "set-cookie": sessionCookie, + }); + return; + } + + const authResult = authStore.verify(getRequestToken(req)); + if (!authResult || authResult.success !== true) { + const expired = authResult && authResult.errorMsg === "X_COMMAND_TOKEN_EXPIRED"; + sendError(res, 401, expired ? "TOKEN_EXPIRED" : "TOKEN_INVALID", expired + ? "This character link expired. Use /x in game for a fresh link." + : "This character link is invalid. Use /x in game for a fresh link."); + return; + } + const authRecord = authResult.data; + const characterID = toPositiveInt(authRecord.characterID, 0); + + if (req.method === "GET" && requestUrl.pathname === "/api/v1/universe/topology") { + const result = universeAdapter.getTopology(); + if (!result || result.success !== true) { + sendError(res, 409, result && result.errorMsg || "UNIVERSE_TOPOLOGY_UNAVAILABLE", "Universe topology is unavailable."); + return; + } + const topologyVersion = String(result.data && result.data.topologyVersion || ""); + const etag = `"xc-universe-${topologyVersion}"`; + const cacheHeaders = { + "cache-control": "private, max-age=3600", + etag, + }; + if (String(req.headers["if-none-match"] || "") === etag) { + res.writeHead(304, cacheHeaders); + res.end(); + return; + } + sendJson(res, 200, { success: true, data: result.data }, cacheHeaders); + return; + } + + if (req.method === "GET" && requestUrl.pathname === "/api/v1/universe/route") { + const result = universeAdapter.getRoute( + requestUrl.searchParams.get("from"), + requestUrl.searchParams.get("to"), + ); + if (!result || result.success !== true) { + sendError(res, 400, result && result.errorMsg || "UNIVERSE_ROUTE_INVALID", String(result && result.message || "The route could not be calculated.")); + return; + } + sendJson(res, 200, { success: true, data: result.data }); + return; + } + + if (req.method === "GET" && requestUrl.pathname === "/api/v1/universe/snapshot") { + const result = universeAdapter.getSnapshot(characterID, { authRecord }); + if (!result || result.success !== true) { + sendError(res, 409, result && result.errorMsg || "UNIVERSE_SNAPSHOT_UNAVAILABLE", String(result && result.message || "The character map snapshot is unavailable.")); + return; + } + sendJson(res, 200, { success: true, data: result.data }); + return; + } + + if (req.method === "GET" && requestUrl.pathname === "/api/v1/session") { + const industrial = industrialAdapter.listForCharacter(characterID); + sendJson(res, 200, { + success: true, + data: { + character: buildCharacterSnapshot(authRecord), + modules: buildModuleManifest(), + industrial: industrial && industrial.success === true ? industrial.data : null, + serverTimeMs: Date.now(), + }, + }); + return; + } + + if (req.method === "GET" && requestUrl.pathname === "/api/v1/industrial") { + const result = industrialAdapter.listForCharacter(characterID); + if (!result || result.success !== true) { + sendError(res, 409, result && result.errorMsg || "INDUSTRIAL_UNAVAILABLE", "Industrial crew state is unavailable."); + return; + } + sendJson(res, 200, { success: true, data: result.data }); + return; + } + + if (req.method === "POST" && requestUrl.pathname === "/api/v1/industrial/action") { + if (!checkPostRateLimit(authRecord.tokenID)) { + sendError(res, 429, "RATE_LIMITED", "Too many orders were issued at once. Wait a few seconds."); + return; + } + let body; + try { + body = await readJsonBody(req); + } catch (error) { + sendError(res, error && error.code === "BODY_TOO_LARGE" ? 413 : 400, error.code || "BODY_INVALID", error.message); + return; + } + const result = industrialAdapter.performAction(characterID, body.action, body); + if (!result || result.success !== true) { + const offline = result && result.errorMsg === "X_COMMAND_CHARACTER_OFFLINE"; + sendError(res, offline ? 409 : 400, result && result.errorMsg || "ORDER_REJECTED", String(result && result.message || "EveJS rejected the order.")); + return; + } + sendJson(res, 200, { success: true, message: result.message, data: result.data }); + return; + } + + sendError(res, 404, "NOT_FOUND", "X Command endpoint not found."); +} + +let server = null; +function startServer() { + if (server) return server; + const host = String(config.xCommandApiBindHost || "127.0.0.1").trim() || "127.0.0.1"; + const port = Math.max(1, Math.min(65535, toPositiveInt(config.xCommandApiPort, 26500))); + server = http.createServer((req, res) => { + Promise.resolve(handleRequest(req, res)).catch((error) => { + log.err(`[XCommand] request failed: ${error && error.message || error}`); + if (!res.headersSent) sendError(res, 500, "SERVER_ERROR", "X Command could not complete the request."); + else res.end(); + }); + }); + server.on("error", (error) => { + log.err(`[XCommand] server error on ${host}:${port}: ${error.message}`); + }); + server.listen(port, host, () => { + log.info(`[XCommand] API listening on http://${host}:${port}`); + }); + return server; +} + +module.exports = { + enabled: config.xCommandEnabled === true, + serviceName: "xCommandServer", + exec() { + startServer(); + }, + __testHooks: { + buildCharacterSnapshot, + checkPostRateLimit, + getAllowedOrigins: (sourceConfig = config) => getAllowedOrigins(sourceConfig), + handleRequest, + normalizeOrigin: normalizeHttpOrigin, + setCorsHeaders: (req, res, sourceConfig = config) => + setCorsHeaders(req, res, sourceConfig), + }, +}; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/staticUi.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/staticUi.js new file mode 100644 index 0000000..f2acbf5 --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/_secondary/xCommand/staticUi.js @@ -0,0 +1,138 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const DEFAULT_UI_ROOT = path.resolve( + __dirname, + "../../../assets/x-command", +); + +const CONTENT_TYPES = Object.freeze({ + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".png": "image/png", + ".svg": "image/svg+xml", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", +}); +const UI_SECURITY_HEADERS = Object.freeze({ + "content-security-policy": + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " + + "img-src 'self' data: https://images.evetech.net; connect-src 'self'; " + + "object-src 'none'; base-uri 'none'; frame-ancestors 'none'", + "x-frame-options": "DENY", +}); + +function isWithinRoot(rootPath, candidatePath) { + const normalizedRoot = path.resolve(rootPath); + const normalizedCandidate = path.resolve(candidatePath); + return normalizedCandidate === normalizedRoot || + normalizedCandidate.startsWith(`${normalizedRoot}${path.sep}`); +} + +function decodePathname(pathname) { + try { + const decoded = decodeURIComponent(String(pathname || "/")); + return decoded.includes("\0") ? null : decoded; + } catch { + return null; + } +} + +function resolveUiAssetPath(rootPath, pathname) { + const decoded = decodePathname(pathname); + if (decoded === null) return null; + + const normalizedRoot = path.resolve(rootPath); + const relativePath = decoded === "/" + ? "index.html" + : decoded.replace(/^[/\\]+/, ""); + const candidatePath = path.resolve(normalizedRoot, relativePath); + return isWithinRoot(normalizedRoot, candidatePath) ? candidatePath : null; +} + +async function statFile(filePath) { + try { + const stat = await fs.promises.stat(filePath); + return stat.isFile() ? stat : null; + } catch { + return null; + } +} + +function getCacheControl(filePath) { + if (path.basename(filePath).toLowerCase() === "index.html") { + return "no-cache"; + } + return /[-.][A-Za-z0-9_-]{8,}\.[^.]+$/.test(path.basename(filePath)) + ? "public, max-age=31536000, immutable" + : "public, max-age=3600"; +} + +async function serveUiRequest(req, res, requestUrl, options = {}) { + if (!req || !res || !requestUrl) return false; + if (!["GET", "HEAD"].includes(String(req.method || "").toUpperCase())) { + return false; + } + if ( + requestUrl.pathname === "/health" || + requestUrl.pathname.startsWith("/api/") + ) { + return false; + } + + const rootPath = path.resolve(options.rootPath || DEFAULT_UI_ROOT); + let filePath = resolveUiAssetPath(rootPath, requestUrl.pathname); + if (!filePath) return false; + + let stat = await statFile(filePath); + if (!stat && !path.extname(requestUrl.pathname)) { + filePath = path.join(rootPath, "index.html"); + stat = await statFile(filePath); + } + if (!stat) return false; + + const contentType = CONTENT_TYPES[path.extname(filePath).toLowerCase()] || + "application/octet-stream"; + res.writeHead(200, { + "content-type": contentType, + "content-length": stat.size, + "cache-control": getCacheControl(filePath), + "x-content-type-options": "nosniff", + "referrer-policy": "no-referrer", + ...UI_SECURITY_HEADERS, + }); + if (String(req.method || "").toUpperCase() === "HEAD") { + res.end(); + return true; + } + fs.createReadStream(filePath) + .on("error", () => { + if (!res.headersSent) { + res.writeHead(500, { "content-type": "text/plain; charset=utf-8" }); + } + res.end(); + }) + .pipe(res); + return true; +} + +module.exports = { + CONTENT_TYPES, + DEFAULT_UI_ROOT, + UI_SECURITY_HEADERS, + decodePathname, + getCacheControl, + isWithinRoot, + resolveUiAssetPath, + serveUiRequest, +}; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandAuth.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandAuth.js new file mode 100644 index 0000000..885117b --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandAuth.js @@ -0,0 +1,493 @@ +"use strict"; + +const crypto = require("crypto"); +const path = require("path"); + +const config = require(path.join(__dirname, "../../config")); +const { + createTableRepository, +} = require(path.join(__dirname, "../../gameStore/tableRepository")); + +const TABLE_NAME = "xCommandAuth"; +const TOKEN_PREFIX = "xc1"; +const LAST_USED_WRITE_INTERVAL_MS = 5 * 60 * 1000; +const DEFAULT_LINK_MINUTES = 5; +const MIN_LINK_MINUTES = 1; +const MAX_LINK_MINUTES = 30; + +function toPositiveInt(value, fallback = 0) { + const numeric = Math.trunc(Number(value) || 0); + return numeric > 0 ? numeric : fallback; +} + +function clone(value) { + return value === undefined ? undefined : JSON.parse(JSON.stringify(value)); +} + +function hashToken(tokenID, secret) { + return crypto + .createHash("sha256") + .update(`${tokenID}:${secret}`, "utf8") + .digest("hex"); +} + +function safeHashEqual(left, right) { + const leftBuffer = Buffer.from(String(left || ""), "hex"); + const rightBuffer = Buffer.from(String(right || ""), "hex"); + return leftBuffer.length > 0 && + leftBuffer.length === rightBuffer.length && + crypto.timingSafeEqual(leftBuffer, rightBuffer); +} + +function parseToken(token) { + const match = String(token || "") + .trim() + .match(/^xc1_([a-f0-9]{24})_([A-Za-z0-9_-]{43})$/); + return match ? { tokenID: match[1], secret: match[2] } : null; +} + +function isSuccess(result) { + return Boolean(result && result.success === true); +} + +function normalizeNowMs(nowMs) { + return Math.max(0, Math.trunc(Number(nowMs) || Date.now())); +} + +function createAuthStore(options = {}) { + const repo = options.repo || createTableRepository("service:xCommand", { + strict: true, + }); + const sourceConfig = options.config || config; + + function read(pathArg, fallback = null) { + const result = repo.read(TABLE_NAME, pathArg); + return result && result.success === true && result.data != null + ? clone(result.data) + : clone(fallback); + } + + function write(pathArg, value) { + return repo.write(TABLE_NAME, pathArg, clone(value)); + } + + function remove(pathArg) { + return repo.remove(TABLE_NAME, pathArg); + } + + function createTokenRecord(input, kind, issuedAtMs, lifetimeMs) { + const characterID = toPositiveInt(input && input.characterID, 0); + const tokenID = crypto.randomBytes(12).toString("hex"); + const secret = crypto.randomBytes(32).toString("base64url"); + const token = `${TOKEN_PREFIX}_${tokenID}_${secret}`; + const record = { + schemaVersion: 2, + kind, + tokenID, + tokenHash: hashToken(tokenID, secret), + characterID, + characterName: String( + input && input.characterName || `Character ${characterID}`, + ).slice(0, 128), + issuedAtMs, + expiresAtMs: issuedAtMs + lifetimeMs, + }; + if (kind === "session") record.lastUsedAtMs = issuedAtMs; + return { token, record }; + } + + function restoreIndex(pathArg, previous) { + if (previous && previous.tokenID) return write(pathArg, previous); + return remove(pathArg); + } + + function revokeTokenID(tokenID) { + const normalizedTokenID = String(tokenID || "").trim(); + if (!normalizedTokenID) { + return { success: true, data: { revoked: false } }; + } + const record = read(`/sessions/${normalizedTokenID}`, null); + if (record && toPositiveInt(record.characterID, 0)) { + const current = read(`/characters/${record.characterID}`, null); + if (current && String(current.tokenID || "") === normalizedTokenID) { + remove(`/characters/${record.characterID}`); + } + } + const result = remove(`/sessions/${normalizedTokenID}`); + return isSuccess(result) + ? { success: true, data: { revoked: Boolean(record), record } } + : result; + } + + function revokeLinkTokenID(tokenID) { + const normalizedTokenID = String(tokenID || "").trim(); + if (!normalizedTokenID) { + return { success: true, data: { revoked: false } }; + } + const record = read(`/links/${normalizedTokenID}`, null); + if (record && toPositiveInt(record.characterID, 0)) { + const current = read(`/pendingLinks/${record.characterID}`, null); + if (current && String(current.tokenID || "") === normalizedTokenID) { + remove(`/pendingLinks/${record.characterID}`); + } + } + const result = remove(`/links/${normalizedTokenID}`); + return isSuccess(result) + ? { success: true, data: { revoked: Boolean(record), record } } + : result; + } + + function revokeCharacter(characterID) { + const normalizedCharacterID = toPositiveInt(characterID, 0); + if (!normalizedCharacterID) { + return { success: false, errorMsg: "X_COMMAND_CHARACTER_REQUIRED" }; + } + const indexRecord = read(`/characters/${normalizedCharacterID}`, null); + const pendingLink = read(`/pendingLinks/${normalizedCharacterID}`, null); + const sessionResult = indexRecord && indexRecord.tokenID + ? revokeTokenID(indexRecord.tokenID) + : { success: true, data: { revoked: false } }; + const linkResult = pendingLink && pendingLink.tokenID + ? revokeLinkTokenID(pendingLink.tokenID) + : { success: true, data: { revoked: false } }; + if (!isSuccess(sessionResult)) return sessionResult; + if (!isSuccess(linkResult)) return linkResult; + return { + success: true, + data: { + revoked: Boolean( + sessionResult.data && sessionResult.data.revoked || + linkResult.data && linkResult.data.revoked, + ), + }, + }; + } + + function issueForCharacter(input = {}, nowMs = Date.now()) { + const characterID = toPositiveInt(input.characterID, 0); + if (!characterID) { + return { success: false, errorMsg: "X_COMMAND_CHARACTER_REQUIRED" }; + } + const issuedAtMs = normalizeNowMs(nowMs); + const lifetimeHours = Math.max( + 1, + Math.min(8760, toPositiveInt(sourceConfig.xCommandSessionHours, 720)), + ); + const current = read(`/characters/${characterID}`, null); + const { token, record } = createTokenRecord( + input, + "session", + issuedAtMs, + lifetimeHours * 60 * 60 * 1000, + ); + const sessionWrite = write(`/sessions/${record.tokenID}`, record); + if (!isSuccess(sessionWrite)) { + return sessionWrite || { + success: false, + errorMsg: "X_COMMAND_AUTH_WRITE_FAILED", + }; + } + const indexWrite = write(`/characters/${characterID}`, { + tokenID: record.tokenID, + characterID, + issuedAtMs, + expiresAtMs: record.expiresAtMs, + }); + if (!isSuccess(indexWrite)) { + remove(`/sessions/${record.tokenID}`); + restoreIndex(`/characters/${characterID}`, current); + return indexWrite || { + success: false, + errorMsg: "X_COMMAND_AUTH_INDEX_WRITE_FAILED", + }; + } + if ( + current && + current.tokenID && + String(current.tokenID) !== record.tokenID + ) { + remove(`/sessions/${current.tokenID}`); + } + return { + success: true, + data: { + token, + characterID, + characterName: record.characterName, + issuedAtMs, + expiresAtMs: record.expiresAtMs, + }, + }; + } + + function issueLinkForCharacter(input = {}, nowMs = Date.now()) { + const characterID = toPositiveInt(input.characterID, 0); + if (!characterID) { + return { success: false, errorMsg: "X_COMMAND_CHARACTER_REQUIRED" }; + } + const issuedAtMs = normalizeNowMs(nowMs); + const lifetimeMinutes = Math.max( + MIN_LINK_MINUTES, + Math.min( + MAX_LINK_MINUTES, + toPositiveInt(sourceConfig.xCommandLinkMinutes, DEFAULT_LINK_MINUTES), + ), + ); + const current = read(`/pendingLinks/${characterID}`, null); + const { token, record } = createTokenRecord( + input, + "exchange", + issuedAtMs, + lifetimeMinutes * 60 * 1000, + ); + const linkWrite = write(`/links/${record.tokenID}`, record); + if (!isSuccess(linkWrite)) { + return linkWrite || { + success: false, + errorMsg: "X_COMMAND_AUTH_WRITE_FAILED", + }; + } + const indexWrite = write(`/pendingLinks/${characterID}`, { + tokenID: record.tokenID, + characterID, + issuedAtMs, + expiresAtMs: record.expiresAtMs, + }); + if (!isSuccess(indexWrite)) { + remove(`/links/${record.tokenID}`); + restoreIndex(`/pendingLinks/${characterID}`, current); + return indexWrite || { + success: false, + errorMsg: "X_COMMAND_AUTH_INDEX_WRITE_FAILED", + }; + } + if ( + current && + current.tokenID && + String(current.tokenID) !== record.tokenID + ) { + remove(`/links/${current.tokenID}`); + } + return { + success: true, + data: { + token, + characterID, + characterName: record.characterName, + issuedAtMs, + expiresAtMs: record.expiresAtMs, + }, + }; + } + + function verify(token, nowMs = Date.now()) { + const parsed = parseToken(token); + if (!parsed) { + return { success: false, errorMsg: "X_COMMAND_TOKEN_INVALID" }; + } + const record = read(`/sessions/${parsed.tokenID}`, null); + if (!record || !safeHashEqual( + record.tokenHash, + hashToken(parsed.tokenID, parsed.secret), + )) { + const linkRecord = read(`/links/${parsed.tokenID}`, null); + if ( + linkRecord && + safeHashEqual( + linkRecord.tokenHash, + hashToken(parsed.tokenID, parsed.secret), + ) + ) { + return { + success: false, + errorMsg: "X_COMMAND_TOKEN_EXCHANGE_ONLY", + }; + } + return { success: false, errorMsg: "X_COMMAND_TOKEN_INVALID" }; + } + if (record.kind !== "session") { + return { success: false, errorMsg: "X_COMMAND_TOKEN_INVALID" }; + } + const current = read(`/characters/${record.characterID}`, null); + if (!current || String(current.tokenID || "") !== parsed.tokenID) { + return { success: false, errorMsg: "X_COMMAND_TOKEN_INVALID" }; + } + const normalizedNowMs = normalizeNowMs(nowMs); + if (toPositiveInt(record.expiresAtMs, 0) <= normalizedNowMs) { + revokeTokenID(parsed.tokenID); + return { success: false, errorMsg: "X_COMMAND_TOKEN_EXPIRED" }; + } + if ( + normalizedNowMs - toPositiveInt(record.lastUsedAtMs, 0) >= + LAST_USED_WRITE_INTERVAL_MS + ) { + const updated = { ...record, lastUsedAtMs: normalizedNowMs }; + write(`/sessions/${parsed.tokenID}`, updated); + record.lastUsedAtMs = normalizedNowMs; + } + return { success: true, data: clone(record) }; + } + + function exchangeLink(token, nowMs = Date.now()) { + const parsed = parseToken(token); + if (!parsed) { + return { success: false, errorMsg: "X_COMMAND_TOKEN_INVALID" }; + } + const linkRecord = read(`/links/${parsed.tokenID}`, null); + if ( + !linkRecord || + linkRecord.kind !== "exchange" || + linkRecord.consumedAtMs || + !safeHashEqual( + linkRecord.tokenHash, + hashToken(parsed.tokenID, parsed.secret), + ) + ) { + return { success: false, errorMsg: "X_COMMAND_TOKEN_INVALID" }; + } + const characterID = toPositiveInt(linkRecord.characterID, 0); + const pending = read(`/pendingLinks/${characterID}`, null); + if (!pending || String(pending.tokenID || "") !== parsed.tokenID) { + return { success: false, errorMsg: "X_COMMAND_TOKEN_INVALID" }; + } + const normalizedNowMs = normalizeNowMs(nowMs); + if (toPositiveInt(linkRecord.expiresAtMs, 0) <= normalizedNowMs) { + revokeLinkTokenID(parsed.tokenID); + return { success: false, errorMsg: "X_COMMAND_TOKEN_EXPIRED" }; + } + + const lifetimeHours = Math.max( + 1, + Math.min(8760, toPositiveInt(sourceConfig.xCommandSessionHours, 720)), + ); + const current = read(`/characters/${characterID}`, null); + const { token: sessionToken, record: sessionRecord } = createTokenRecord( + { + characterID, + characterName: linkRecord.characterName, + }, + "session", + normalizedNowMs, + lifetimeHours * 60 * 60 * 1000, + ); + const sessionWrite = write( + `/sessions/${sessionRecord.tokenID}`, + sessionRecord, + ); + if (!isSuccess(sessionWrite)) { + return sessionWrite || { + success: false, + errorMsg: "X_COMMAND_AUTH_WRITE_FAILED", + }; + } + + const consumeWrite = write(`/links/${parsed.tokenID}`, { + ...linkRecord, + consumedAtMs: normalizedNowMs, + }); + if (!isSuccess(consumeWrite)) { + remove(`/sessions/${sessionRecord.tokenID}`); + return consumeWrite || { + success: false, + errorMsg: "X_COMMAND_LINK_CONSUME_FAILED", + }; + } + + const indexWrite = write(`/characters/${characterID}`, { + tokenID: sessionRecord.tokenID, + characterID, + issuedAtMs: normalizedNowMs, + expiresAtMs: sessionRecord.expiresAtMs, + }); + if (!isSuccess(indexWrite)) { + write(`/links/${parsed.tokenID}`, linkRecord); + remove(`/sessions/${sessionRecord.tokenID}`); + restoreIndex(`/characters/${characterID}`, current); + return indexWrite || { + success: false, + errorMsg: "X_COMMAND_AUTH_INDEX_WRITE_FAILED", + }; + } + + remove(`/pendingLinks/${characterID}`); + remove(`/links/${parsed.tokenID}`); + if ( + current && + current.tokenID && + String(current.tokenID) !== sessionRecord.tokenID + ) { + remove(`/sessions/${current.tokenID}`); + } + return { + success: true, + data: { + token: sessionToken, + characterID, + characterName: sessionRecord.characterName, + issuedAtMs: normalizedNowMs, + expiresAtMs: sessionRecord.expiresAtMs, + }, + }; + } + + function getCharacterStatus(characterID, nowMs = Date.now()) { + const normalizedCharacterID = toPositiveInt(characterID, 0); + const indexRecord = normalizedCharacterID + ? read(`/characters/${normalizedCharacterID}`, null) + : null; + if (!indexRecord || !indexRecord.tokenID) { + return { linked: false, characterID: normalizedCharacterID }; + } + const record = read(`/sessions/${indexRecord.tokenID}`, null); + const expiresAtMs = toPositiveInt(record && record.expiresAtMs, 0); + if ( + !record || + record.kind !== "session" || + expiresAtMs <= normalizeNowMs(nowMs) + ) { + revokeTokenID(indexRecord.tokenID); + return { linked: false, characterID: normalizedCharacterID }; + } + return { + linked: true, + characterID: normalizedCharacterID, + characterName: record.characterName, + issuedAtMs: record.issuedAtMs, + expiresAtMs, + lastUsedAtMs: record.lastUsedAtMs, + }; + } + + return Object.freeze({ + TABLE_NAME, + exchangeLink, + getCharacterStatus, + issueForCharacter, + issueLinkForCharacter, + parseToken, + revokeCharacter, + revokeLinkTokenID, + revokeTokenID, + verify, + }); +} + +let defaultStore = null; +function getDefaultAuthStore() { + if (!defaultStore) { + defaultStore = createAuthStore(); + } + return defaultStore; +} + +module.exports = { + DEFAULT_LINK_MINUTES, + MAX_LINK_MINUTES, + MIN_LINK_MINUTES, + TABLE_NAME, + TOKEN_PREFIX, + createAuthStore, + getDefaultAuthStore, + hashToken, + parseToken, +}; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandChatCommand.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandChatCommand.js new file mode 100644 index 0000000..e8aa11d --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandChatCommand.js @@ -0,0 +1,140 @@ +"use strict"; + +const path = require("path"); +const config = require(path.join(__dirname, "../../config")); +const { getDefaultAuthStore } = require(path.join(__dirname, "./xCommandAuth")); + +function toPositiveInt(value, fallback = 0) { + const numeric = Math.trunc(Number(value) || 0); + return numeric > 0 ? numeric : fallback; +} + +function parseConfiguredUrl(value, label) { + const raw = String(value || "").trim(); + if (!raw) { + return { success: false, message: `${label} is not configured.` }; + } + try { + const parsed = new URL(raw); + if (!/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]+\/?$/.test(raw)) { + return { + success: false, + message: `${label} must contain only an origin, without a path, query, or fragment.`, + }; + } + if (parsed.username || parsed.password) { + return { + success: false, + message: `${label} must not include URL credentials.`, + }; + } + const localHttp = parsed.protocol === "http:" && + ["localhost", "127.0.0.1", "::1", "[::1]"].includes(parsed.hostname); + if (parsed.protocol !== "https:" && !localHttp) { + return { success: false, message: `${label} must use HTTPS unless it is loopback-only.` }; + } + return { success: true, url: new URL(`${parsed.origin}/`) }; + } catch { + return { success: false, message: `${label} is not a valid absolute URL.` }; + } +} + +function getDefaultLocalUrl(sourceConfig = config) { + const configuredHost = String( + sourceConfig.xCommandApiBindHost || "127.0.0.1", + ).trim(); + const host = ["", "0.0.0.0", "::"].includes(configuredHost) + ? "127.0.0.1" + : configuredHost === "::1" + ? "[::1]" + : configuredHost; + const port = Math.max( + 1, + Math.min(65535, toPositiveInt(sourceConfig.xCommandApiPort, 26500)), + ); + return `http://${host}:${port}/`; +} + +function resolveInterfaceUrl(sourceConfig = config) { + const configured = String(sourceConfig.xCommandPublicUrl || "").trim(); + return parseConfiguredUrl( + configured || getDefaultLocalUrl(sourceConfig), + "xCommandPublicUrl", + ); +} + +function resolveApiUrl(sourceConfig, interfaceUrl) { + const configured = String(sourceConfig.xCommandApiPublicUrl || "").trim(); + return parseConfiguredUrl( + configured || new URL("/", interfaceUrl).toString(), + "xCommandApiPublicUrl", + ); +} + +function executeXCommandChatCommand(session, argumentText = "", options = {}) { + const sourceConfig = options.config || config; + if (sourceConfig.xCommandEnabled !== true) { + return { success: false, message: "X Command is disabled on this server." }; + } + const characterID = toPositiveInt(session && session.characterID, 0); + if (!characterID) { + return { success: false, message: "Select a character before opening X Command." }; + } + const authStore = options.authStore || getDefaultAuthStore(); + const command = String(argumentText || "").trim().toLowerCase() || "open"; + if (["help", "?"].includes(command)) { + return { success: true, message: "X Command: /x opens a fresh one-click character link; /x status shows its expiry; /x reset revokes it." }; + } + if (command === "status") { + const status = authStore.getCharacterStatus(characterID, options.nowMs); + return { + success: true, + message: status.linked + ? `X Command is linked to ${status.characterName}. The current link expires ${new Date(status.expiresAtMs).toISOString()}. Use /x to replace it or /x reset to revoke it.` + : "This character does not have an active X Command link. Use /x to create one.", + }; + } + if (command === "reset") { + const revoked = authStore.revokeCharacter(characterID); + return revoked && revoked.success === true + ? { success: true, message: "X Command link revoked. Use /x whenever you want a new one." } + : { success: false, message: "EveJS could not revoke the X Command link." }; + } + if (command !== "open") { + return { success: false, message: "Usage: /x [open|status|reset]" }; + } + + const interfaceUrl = resolveInterfaceUrl(sourceConfig); + if (!interfaceUrl.success) { + return { success: false, message: interfaceUrl.message }; + } + const apiUrl = resolveApiUrl(sourceConfig, interfaceUrl.url); + if (!apiUrl.success) { + return { success: false, message: apiUrl.message }; + } + const issued = authStore.issueLinkForCharacter({ + characterID, + characterName: String(session.characterName || session.charname || `Character ${characterID}`), + }, options.nowMs); + if (!issued || issued.success !== true) { + return { success: false, message: "EveJS could not create the X Command character link." }; + } + const fragment = new URLSearchParams(interfaceUrl.url.hash.replace(/^#/, "")); + fragment.set("xc_api", apiUrl.url.toString()); + fragment.set("xc_token", issued.data.token); + interfaceUrl.url.hash = fragment.toString(); + return { + success: true, + message: + `Open X Command for ${issued.data.characterName}: ${interfaceUrl.url.toString()} ` + + "This one-time sign-in link expires shortly. An already signed-in browser stays connected until this link is opened.", + }; +} + +module.exports = { + executeXCommandChatCommand, + getDefaultLocalUrl, + parseConfiguredUrl, + resolveApiUrl, + resolveInterfaceUrl, +}; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandIndustrialAdapter.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandIndustrialAdapter.js new file mode 100644 index 0000000..dc4523c --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandIndustrialAdapter.js @@ -0,0 +1,883 @@ +"use strict"; + +const path = require("path"); +const config = require(path.join(__dirname, "../../config")); +const sessionRegistry = require(path.join(__dirname, "../chat/sessionRegistry")); +const { + getDockedLocationID, + isDockedSession, +} = require(path.join(__dirname, "../structure/structureLocation")); +const { + getDefaultContractService, +} = require(path.join(__dirname, "../industrialHirelings/industrialHirelingContracts")); +const { + isIndustrialMiningCrewSecurityAllowed, + listIndustrialMiningCrewPackages, + resolveIndustrialMiningCrewPackage, +} = require(path.join(__dirname, "../industrialHirelings/industrialMiningCrewCatalog")); +const { + getDefaultIndustrialHirelingRuntime, +} = require(path.join(__dirname, "../industrialHirelings/industrialHirelingRuntime")); +const { + getDefaultIndustrialMiningSiteCatalog, +} = require(path.join(__dirname, "../industrialHirelings/industrialHirelingSites")); +const { + executeIndustrialHirelingCommand, +} = require(path.join(__dirname, "../industrialHirelings/industrialHirelingCommand")); +const { + formatBookmarkMessage, + syncContractBookmark, +} = require(path.join(__dirname, "../industrialHirelings/industrialHirelingBookmarkLifecycle")); + +function toPositiveInt(value, fallback = 0) { + const numeric = Math.trunc(Number(value) || 0); + return numeric > 0 ? numeric : fallback; +} + +function toNonNegativeNumber(value, fallback = 0) { + const numeric = Number(value); + return Number.isFinite(numeric) && numeric >= 0 ? numeric : fallback; +} + +function normalizePositiveIntArray(values) { + return [...new Set( + (Array.isArray(values) ? values : []) + .map((value) => toPositiveInt(value, 0)) + .filter(Boolean), + )]; +} + +function sanitizeCrewPackage(packageDefinition) { + if (!packageDefinition || typeof packageDefinition !== "object") { + return null; + } + const crewTypeID = String(packageDefinition.crewTypeID || "").trim(); + if (!crewTypeID) return null; + const memberSlots = (Array.isArray(packageDefinition.memberSlots) + ? packageDefinition.memberSlots + : []) + .map((member) => ({ + slotID: String(member && member.slotID || ""), + role: String(member && member.role || ""), + isCrewChief: Boolean(member && member.isCrewChief === true), + shipTypeID: toPositiveInt(member && member.shipTypeID, 0), + shipTypeName: String(member && member.shipTypeName || ""), + })) + .filter((member) => member.slotID && member.role && member.shipTypeID); + const pricing = packageDefinition.pricing && typeof packageDefinition.pricing === "object" + ? packageDefinition.pricing + : {}; + const security = packageDefinition.security && typeof packageDefinition.security === "object" + ? packageDefinition.security + : {}; + const allowedSecurityClasses = [...new Set( + (Array.isArray(security.allowedSecurityClasses) + ? security.allowedSecurityClasses + : []) + .map((value) => String(value || "").trim().toLowerCase()) + .filter((value) => ["highsec", "lowsec", "nullsec"].includes(value)), + )]; + return { + crewTypeID, + tier: toPositiveInt(packageDefinition.tier, 0), + tierLabel: String(packageDefinition.tierLabel || ""), + name: String(packageDefinition.name || crewTypeID), + composition: String(packageDefinition.composition || ""), + hullCount: Math.max( + memberSlots.length, + toPositiveInt(packageDefinition.hullCount, memberSlots.length), + ), + memberSlots, + pricing: { + mobilizationFeeISK: toNonNegativeNumber(pricing.mobilizationFeeISK, 0), + refundableLossEscrowISK: toNonNegativeNumber(pricing.refundableLossEscrowISK, 0), + activePayrollISKPerHour: toNonNegativeNumber(pricing.activePayrollISKPerHour, 0), + upfrontISK: toNonNegativeNumber( + pricing.upfrontISK, + toNonNegativeNumber(pricing.mobilizationFeeISK, 0) + + toNonNegativeNumber(pricing.refundableLossEscrowISK, 0), + ), + }, + security: { + allowedSecurityClasses, + highSecurityAllowed: allowedSecurityClasses.includes("highsec"), + lowSecurityAllowed: allowedSecurityClasses.includes("lowsec"), + nullSecurityAllowed: allowedSecurityClasses.includes("nullsec"), + }, + }; +} + +function sanitizeCrewMember(member) { + if (!member || typeof member !== "object") return null; + const memberID = String(member.memberID || "").trim(); + const slotID = String(member.slotID || "").trim(); + if (!memberID || !slotID) return null; + const runtimeEntityID = toPositiveInt(member.runtimeEntityID, 0); + return { + memberID, + slotID, + role: String(member.role || ""), + isCrewChief: Boolean(member.isCrewChief === true), + pilotName: String(member.pilotName || ""), + shipName: String(member.shipName || ""), + shipTypeID: toPositiveInt(member.shipTypeID, 0), + shipTypeName: String(member.shipTypeName || ""), + state: String(member.state || (runtimeEntityID > 0 ? "materialized" : "unmaterialized")), + runtimeEntityID, + }; +} + +function getSessionLocation(session) { + const space = session && session._space || {}; + const dockedLocationID = toPositiveInt( + session && (session.stationid || session.stationID || session.structureid || session.structureID), + 0, + ); + return { + online: Boolean(session), + docked: dockedLocationID > 0, + dockedLocationID, + solarSystemID: toPositiveInt(space.systemID || session && (session.solarsystemid2 || session.solarsystemid), 0), + shipID: toPositiveInt(space.shipID || session && session.shipid, 0), + }; +} + +function getCharacterSession(characterID) { + return sessionRegistry.findSessionByCharacterID(toPositiveInt(characterID, 0)); +} + +function resolveContractPhase(contract) { + const state = String(contract && contract.state || "").trim().toLowerCase(); + const navigationPhase = String( + contract && contract.navigation && contract.navigation.phase || "", + ).trim().toLowerCase(); + const order = String(contract && contract.order || "").trim().toLowerCase(); + const role = String(contract && contract.role || "").trim().toLowerCase(); + const cargoStatus = String(contract && contract.cargoStatus || "").trim().toLowerCase(); + if (state && state !== "active") return state; + if (navigationPhase === "in_transit" || navigationPhase === "blocked") { + return navigationPhase; + } + if (["reserved", "collecting", "in_transit", "recovery_required"].includes(cargoStatus)) { + return cargoStatus; + } + if (toPositiveInt(contract && contract.runtimeFleetID, 0) > 0) { + return role === "miner" ? "mining" : "collecting"; + } + if (order === "support_mining" || order === "haul_operation") { + return "awaiting_deployment"; + } + if (order === "return_home") return "returning_home"; + if (order === "emergency_withdraw") return "withdrawing"; + return "standby"; +} + +function sanitizeContract(contractArg, options = {}) { + const contract = contractArg && typeof contractArg === "object" ? contractArg : {}; + const siteCatalog = options.siteCatalog || getDefaultIndustrialMiningSiteCatalog(); + const assignedSiteID = toPositiveInt(contract.assignedSiteID, 0); + const assignedSystemID = toPositiveInt(contract.assignedSystemID, 0); + const siteDescription = assignedSiteID > 0 + ? siteCatalog.describeSite(assignedSiteID) + : null; + const systemDescription = assignedSystemID > 0 + ? siteCatalog.describeSystem(assignedSystemID) + : null; + const serial = toPositiveInt(contract.serial, 0); + const role = String(contract.role || ""); + const contractKind = String(contract.contractKind || ""); + const isMiningCrew = contractKind === "mining_crew"; + const crewPackage = isMiningCrew + ? resolveIndustrialMiningCrewPackage(contract.crewTypeID) + : null; + const members = isMiningCrew + ? (Array.isArray(contract.members) ? contract.members : []) + .map(sanitizeCrewMember) + .filter(Boolean) + : []; + const chief = members.find((member) => member.isCrewChief) || members[0] || null; + const pilotName = String( + chief && chief.pilotName || + contract.pilotName || + contract.identityName || + "", + ).trim(); + const crewName = String( + contract.crewName || + contract.packageName || + crewPackage && crewPackage.name || + "", + ).trim(); + const displayName = isMiningCrew + ? crewName || `Mining Crew${serial > 0 ? ` #${serial}` : ""}` + : pilotName || + `${role === "miner" ? "Miner" : role === "hauler" ? "Hauler" : "Industrial"} Contract${serial > 0 ? ` #${serial}` : ""}`; + const memberRuntimeIDsByRole = { + miner: normalizePositiveIntArray( + members + .filter((member) => member.role === "miner") + .map((member) => member.runtimeEntityID), + ), + mining_support: normalizePositiveIntArray( + members + .filter((member) => member.role === "mining_support") + .map((member) => member.runtimeEntityID), + ), + hauler: normalizePositiveIntArray( + members + .filter((member) => member.role === "hauler") + .map((member) => member.runtimeEntityID), + ), + }; + const minerEntityIDs = normalizePositiveIntArray([ + ...(Array.isArray(contract.minerEntityIDs) ? contract.minerEntityIDs : []), + ...memberRuntimeIDsByRole.miner, + ]); + const supportEntityIDs = normalizePositiveIntArray([ + ...(Array.isArray(contract.supportEntityIDs) ? contract.supportEntityIDs : []), + ...memberRuntimeIDsByRole.mining_support, + ]); + const haulerEntityIDs = normalizePositiveIntArray([ + ...(Array.isArray(contract.haulerEntityIDs) ? contract.haulerEntityIDs : []), + ...memberRuntimeIDsByRole.hauler, + ]); + const deployedEntityIDs = normalizePositiveIntArray([ + ...(Array.isArray(contract.runtimeEntityIDs) ? contract.runtimeEntityIDs : []), + ...minerEntityIDs, + ...supportEntityIDs, + ...haulerEntityIDs, + ]); + const hullCount = isMiningCrew + ? Math.max( + members.length, + toPositiveInt(contract.hullCount, 0), + toPositiveInt(crewPackage && crewPackage.hullCount, 0), + ) + : 1; + const rawNavigation = + contract.navigation && typeof contract.navigation === "object" + ? contract.navigation + : {}; + const routeSystemIDs = (Array.isArray(rawNavigation.routeSystemIDs) + ? rawNavigation.routeSystemIDs + : []) + .map((value) => toPositiveInt(value, 0)) + .filter(Boolean); + const legIndex = Math.max( + 0, + Math.min( + Math.max(0, routeSystemIDs.length - 1), + Math.trunc(Number(rawNavigation.legIndex) || 0), + ), + ); + const navigationPhase = String(rawNavigation.phase || "docked"); + const nextLegArrivalAtMs = Math.max(0, Number(rawNavigation.arrivalAtMs) || 0); + const remainingLegs = Math.max(0, routeSystemIDs.length - 1 - legIndex); + const legDurationMs = Math.max(0, Number(rawNavigation.legDurationMs) || 0); + const etaAtMs = navigationPhase === "in_transit" && remainingLegs > 0 + ? nextLegArrivalAtMs + (Math.max(0, remainingLegs - 1) * legDurationMs) + : Math.max( + nextLegArrivalAtMs, + Number(rawNavigation.completedAtMs) || 0, + ); + return { + contractID: String(contract.contractID || ""), + serial, + role, + contractKind, + crewTypeID: isMiningCrew ? String(contract.crewTypeID || "") : "", + crewName: isMiningCrew ? crewName : "", + crewComposition: isMiningCrew + ? String( + contract.composition || + crewPackage && crewPackage.composition || + "", + ) + : "", + crewTier: isMiningCrew + ? toPositiveInt(contract.tier, toPositiveInt(crewPackage && crewPackage.tier, 0)) + : 0, + crewTierLabel: isMiningCrew + ? String(contract.tierLabel || crewPackage && crewPackage.tierLabel || "") + : "", + hullCount, + deployedCount: deployedEntityIDs.length, + runtimeCounts: { + total: deployedEntityIDs.length, + miners: minerEntityIDs.length, + miningSupport: supportEntityIDs.length, + haulers: haulerEntityIDs.length, + expectedTotal: hullCount, + }, + members, + state: String(contract.state || ""), + order: String(contract.order || ""), + displayName, + contractName: displayName, + phase: resolveContractPhase(contract), + pilotName, + shipName: String(chief && chief.shipName || contract.shipName || ""), + shipTypeID: toPositiveInt( + chief && chief.shipTypeID, + toPositiveInt(contract.shipTypeID, 0), + ), + homeStationID: toPositiveInt(contract.homeStationID, 0), + destinationStationID: toPositiveInt(contract.destinationStationID, 0), + currentSystemID: toPositiveInt( + rawNavigation.currentSystemID, + toPositiveInt(contract.currentSystemID, assignedSystemID), + ), + navigation: { + currentSystemID: toPositiveInt( + rawNavigation.currentSystemID, + toPositiveInt(contract.currentSystemID, assignedSystemID), + ), + phase: navigationPhase, + routeSystemIDs, + legIndex, + etaAtMs, + revision: Math.max(0, Math.trunc(Number(rawNavigation.revision) || 0)), + }, + assignedSystemID, + assignedSystemName: String(systemDescription && systemDescription.systemName || ""), + assignedSiteID, + assignedSiteName: String(siteDescription && siteDescription.siteName || ""), + assignedTargetID: toPositiveInt(contract.assignedTargetID, 0), + runtimeFleetID: toPositiveInt(contract.runtimeFleetID, 0), + runtimeShipItemID: toPositiveInt(contract.runtimeShipItemID, 0), + cargoStatus: String(contract.cargoStatus || "empty"), + cargoVolumeM3: Math.max(0, Number(contract.cargoVolumeM3) || 0), + cargoCapacityM3: Math.max(0, Number(contract.cargoCapacityM3) || 0), + expiresAtMs: Math.max(0, Number(contract.expiresAtMs) || 0), + updatedAtMs: Math.max(0, Number(contract.updatedAtMs) || 0), + }; +} + +function buildOperationalSystems(contractsArg, locationArg, siteCatalogArg) { + const contracts = Array.isArray(contractsArg) ? contractsArg : []; + const location = locationArg && typeof locationArg === "object" ? locationArg : {}; + const siteCatalog = siteCatalogArg || getDefaultIndustrialMiningSiteCatalog(); + const systemIDs = new Set(); + const currentSystemID = toPositiveInt(location.solarSystemID, 0); + if (currentSystemID > 0) systemIDs.add(currentSystemID); + const assignedContractIDsBySiteID = new Map(); + for (const contract of contracts) { + const assignedSystemID = toPositiveInt(contract && contract.assignedSystemID, 0); + if (assignedSystemID > 0) systemIDs.add(assignedSystemID); + for (const locationID of [ + contract && contract.homeStationID, + contract && contract.destinationStationID, + ]) { + const stationSystemID = siteCatalog.getSystemIDForLocation(locationID); + if (stationSystemID > 0) systemIDs.add(stationSystemID); + } + const assignedSiteID = toPositiveInt(contract && contract.assignedSiteID, 0); + const contractID = String(contract && contract.contractID || "").trim(); + if (assignedSiteID > 0 && contractID) { + if (!assignedContractIDsBySiteID.has(assignedSiteID)) { + assignedContractIDsBySiteID.set(assignedSiteID, []); + } + assignedContractIDsBySiteID.get(assignedSiteID).push(contractID); + } + } + return siteCatalog.surveySystems([...systemIDs], { assignedContractIDsBySiteID }); +} + +function listForCharacter(characterID, options = {}) { + const activeConfig = options.config || config; + const session = options.session || getCharacterSession(characterID); + const crewModeEnabled = activeConfig.industrialMiningCrewsEnabled === true; + const economyEnabled = crewModeEnabled && + activeConfig.industrialMiningCrewsEconomyEnabled === true; + const crewPackages = crewModeEnabled + ? listIndustrialMiningCrewPackages() + .map(sanitizeCrewPackage) + .filter(Boolean) + : []; + if ( + activeConfig.xCommandIndustrialEnabled === false || + activeConfig.livingUniverseEnabled !== true || + activeConfig.industrialHirelingsEnabled !== true + ) { + return { + success: true, + data: { + enabled: false, + crewModeEnabled, + economyEnabled, + crewPackages, + contracts: [], + systems: [], + location: getSessionLocation(session), + }, + }; + } + const service = options.contractService || getDefaultContractService(); + const siteCatalog = options.siteCatalog || getDefaultIndustrialMiningSiteCatalog(); + let reconciliation = null; + if (session && options.reconcileRuntime !== false) { + const runtime = options.hirelingRuntime || getDefaultIndustrialHirelingRuntime(); + if (runtime && typeof runtime.reconcileForSession === "function") { + reconciliation = runtime.reconcileForSession(session, { nowMs: options.nowMs }); + } + } + const listed = service.listForCharacter(toPositiveInt(characterID, 0)); + if (!listed || listed.success !== true) { + return listed || { success: false, errorMsg: "X_COMMAND_INDUSTRIAL_LIST_FAILED" }; + } + const contracts = listed.data.contracts || []; + const location = getSessionLocation(session); + return { + success: true, + data: { + enabled: listed.data.enabled === true, + crewModeEnabled, + economyEnabled, + crewPackages, + contracts: contracts.map((contract) => sanitizeContract(contract, { siteCatalog })), + systems: activeConfig.industrialHirelingsRemoteSitesEnabled === true + ? buildOperationalSystems(contracts, location, siteCatalog) + : [], + location, + reconciliation: reconciliation && reconciliation.success === true + ? reconciliation.data || null + : reconciliation + ? { errorMsg: String(reconciliation.errorMsg || "INDUSTRIAL_HIRELING_RECONCILE_FAILED") } + : null, + }, + }; +} + +const SIMPLE_ACTIONS = Object.freeze({ + pause: "pause", + resume: "resume", + dismiss: "dismiss", +}); +const ORDERS = new Set(["mine", "haul", "standby", "home", "withdraw"]); + +function runCommand(characterID, commandText, options = {}) { + const session = options.session || getCharacterSession(characterID); + if (!session) { + return { success: false, errorMsg: "X_COMMAND_CHARACTER_OFFLINE", message: "Log this character into EveJS before issuing an order." }; + } + const result = executeIndustrialHirelingCommand(session, commandText, options); + if (!result || result.success !== true) { + return { + success: false, + errorMsg: "X_COMMAND_INDUSTRIAL_ORDER_REJECTED", + message: String(result && result.message || "The order was rejected by EveJS."), + }; + } + return { + success: true, + message: String(result.message || "Order accepted."), + data: listForCharacter(characterID, { ...options, session }).data, + }; +} + +function hireIndustrialContract(characterID, input = {}, options = {}) { + const activeConfig = options.config || config; + const session = options.session || getCharacterSession(characterID); + if (!session) { + return { + success: false, + errorMsg: "X_COMMAND_CHARACTER_OFFLINE", + message: "Log this character into EveJS before hiring an industrial crew.", + }; + } + if (!isDockedSession(session)) { + return { + success: false, + errorMsg: "X_COMMAND_HIRING_REQUIRES_DOCKING", + message: "Dock the linked character before hiring an industrial crew.", + }; + } + const service = options.contractService || getDefaultContractService(); + const runtime = options.hirelingRuntime || getDefaultIndustrialHirelingRuntime(); + const requestedCrewTypeID = String(input.crewTypeID || "").trim().toLowerCase(); + const crewPackage = requestedCrewTypeID + ? resolveIndustrialMiningCrewPackage(requestedCrewTypeID) + : null; + const requestedRole = String(input.role || "").trim().toLowerCase(); + const crewModeEnabled = activeConfig.industrialMiningCrewsEnabled === true; + const legacyHiringDisabled = + crewModeEnabled && + activeConfig.industrialMiningCrewsLegacyHiringEnabled === false; + if (requestedCrewTypeID && !crewPackage) { + return { + success: false, + errorMsg: "X_COMMAND_CREW_PACKAGE_INVALID", + message: "Choose a valid complete mining crew tier.", + }; + } + if (crewPackage && !crewModeEnabled) { + return { + success: false, + errorMsg: "X_COMMAND_MINING_CREWS_DISABLED", + message: "Complete mining crews are disabled on this server.", + }; + } + if (!crewPackage && !["miner", "hauler"].includes(requestedRole)) { + return { + success: false, + errorMsg: "X_COMMAND_CREW_PACKAGE_REQUIRED", + message: crewModeEnabled + ? "Choose a complete mining crew tier." + : "Choose miner or hauler.", + }; + } + if (!crewPackage && legacyHiringDisabled) { + return { + success: false, + errorMsg: "X_COMMAND_LEGACY_HIRING_DISABLED", + message: "Individual miner and hauler hires are disabled. Choose a complete crew tier.", + }; + } + const normalizedCharacterID = toPositiveInt(characterID, 0); + const result = service.hire({ + ownerCharacterID: normalizedCharacterID, + ownerCorporationID: toPositiveInt(session.corporationID, 0), + ownerAllianceID: toPositiveInt( + session.allianceID || session.allianceid, + 0, + ), + ownerWarFactionID: toPositiveInt( + session.warFactionID || session.warfactionid, + 0, + ), + homeStationID: getDockedLocationID(session), + currentSystemID: getSessionLocation(session).solarSystemID, + commandID: input.commandID, + ...(crewPackage + ? { crewTypeID: crewPackage.crewTypeID } + : { role: requestedRole }), + }, options.nowMs); + if (!result || result.success !== true) { + const errorMsg = String(result && result.errorMsg || "INDUSTRIAL_HIRELING_HIRE_FAILED"); + const messages = { + INDUSTRIAL_MINING_CREWS_DISABLED: + "Complete mining crews are disabled on this server.", + INDUSTRIAL_MINING_CREW_PACKAGE_NOT_FOUND: + "Choose a valid complete mining crew tier.", + INDUSTRIAL_HIRELING_LEGACY_HIRING_DISABLED: + "Individual miner and hauler hires are disabled. Choose a complete crew tier.", + INDUSTRIAL_HIRELING_CONTRACT_LIMIT_REACHED: + "This character already has the maximum number of active industrial contracts.", + INDUSTRIAL_HIRELING_HIRE_COMMAND_ID_INVALID: + "The hire command identifier is invalid. Refresh X Command and try again.", + INDUSTRIAL_HIRELING_HIRE_COMMAND_CONFLICT: + "That hire command was already used for a different crew request. Refresh X Command and try again.", + }; + return { + success: false, + errorMsg: "X_COMMAND_INDUSTRIAL_HIRE_REJECTED", + message: messages[errorMsg] || `The industrial contract was rejected (${errorMsg}).`, + }; + } + let presenceResult = null; + if ( + ["active", "paused"].includes(String(result.data && result.data.state || "")) && + runtime && + typeof runtime.reconcileContractPresence === "function" + ) { + presenceResult = runtime.reconcileContractPresence(result.data, { + nowMs: options.nowMs, + }); + if ( + presenceResult && + presenceResult.success === true && + presenceResult.data && + presenceResult.data.contract + ) { + result.data = presenceResult.data.contract; + } + } + const hiredName = crewPackage + ? `${crewPackage.name}: ${crewPackage.composition}` + : `${requestedRole} contract`; + const listed = listForCharacter(normalizedCharacterID, { + ...options, + session, + contractService: service, + }); + return { + success: true, + message: result.replayed === true + ? `${hiredName} commissioning was already processed; no duplicate contract was created.` + : `${hiredName} commissioned at station ${getDockedLocationID(session)}.` + + ( + presenceResult && presenceResult.success !== true + ? " Pilot presence will retry automatically." + : "" + ), + data: listed && listed.success === true ? listed.data : null, + }; +} + +function assignMinerToSite(characterID, selector, input = {}, options = {}) { + const activeConfig = options.config || config; + if (activeConfig.xCommandIndustrialEnabled === false) { + return { + success: false, + errorMsg: "X_COMMAND_INDUSTRIAL_DISABLED", + message: "Industrial controls are disabled in X Command on this server.", + }; + } + if (activeConfig.industrialHirelingsRemoteSitesEnabled !== true) { + return { + success: false, + errorMsg: "X_COMMAND_REMOTE_SITES_DISABLED", + message: "Remote industrial site assignments are disabled on this server.", + }; + } + const session = options.session || getCharacterSession(characterID); + if (!session) { + return { + success: false, + errorMsg: "X_COMMAND_CHARACTER_OFFLINE", + message: "Log this character into EveJS before issuing an order.", + }; + } + const siteID = toPositiveInt(input.siteID || input.assignedSiteID, 0); + if (!siteID) { + return { + success: false, + errorMsg: "X_COMMAND_SITE_REQUIRED", + message: "Choose a valid mining site.", + }; + } + const service = options.contractService || getDefaultContractService(); + const runtime = options.hirelingRuntime || getDefaultIndustrialHirelingRuntime(); + const listed = service.listForCharacter(toPositiveInt(characterID, 0)); + if (!listed || listed.success !== true) { + return listed || { success: false, errorMsg: "X_COMMAND_INDUSTRIAL_LIST_FAILED" }; + } + const contract = (listed.data.contracts || []).find((entry) => ( + String(entry && entry.contractID || "") === selector + )); + if (!contract) { + return { + success: false, + errorMsg: "X_COMMAND_CONTRACT_REQUIRED", + message: "Choose a valid hireling contract.", + }; + } + const isMiningCrew = String(contract.contractKind || "") === "mining_crew"; + const isLegacyMiner = + !isMiningCrew && + String(contract.role || "") === "miner"; + if (!isMiningCrew && !isLegacyMiner) { + return { + success: false, + errorMsg: "X_COMMAND_SITE_REQUIRES_MINER", + message: "Only a mining crew or legacy miner contract can be assigned to an asteroid belt.", + }; + } + const siteCatalog = options.siteCatalog || getDefaultIndustrialMiningSiteCatalog(); + const validation = siteCatalog.validateSite(siteID, 0); + if (!validation || validation.success !== true) { + return { + success: false, + errorMsg: "X_COMMAND_SITE_INVALID", + message: "That asteroid belt is not available in the EveJS universe data.", + }; + } + if (isMiningCrew) { + const crewPackage = resolveIndustrialMiningCrewPackage(contract.crewTypeID); + if (!crewPackage) { + return { + success: false, + errorMsg: "X_COMMAND_CREW_PACKAGE_INVALID", + message: "This crew contract refers to a mining package that is no longer available.", + }; + } + const systemDescription = validation.data && validation.data.systemDescription; + if ( + !isIndustrialMiningCrewSecurityAllowed( + crewPackage.crewTypeID, + systemDescription, + ) + ) { + const allowed = crewPackage.security.allowedSecurityClasses.join(", "); + return { + success: false, + errorMsg: "X_COMMAND_CREW_SECURITY_RESTRICTED", + message: + `${crewPackage.name} cannot deploy in this system's security class. ` + + `Allowed: ${allowed || "none"}.`, + }; + } + } + const assignmentInput = { + ownerCharacterID: toPositiveInt(characterID, 0), + contractID: selector, + assignedSiteID: siteID, + commandID: input.commandID, + }; + let assigned = service.assignSite(assignmentInput, options.nowMs); + if ( + assigned && + assigned.success !== true && + assigned.errorMsg === "INDUSTRIAL_HIRELING_SITE_REASSIGN_REQUIRES_STAND_DOWN" + ) { + const standDown = runtime.standDown(session, contract, { nowMs: options.nowMs }); + if (!standDown || standDown.success !== true) { + return { + success: false, + errorMsg: "X_COMMAND_SITE_REASSIGN_FAILED", + message: "The current mining operation could not be stood down safely.", + }; + } + assigned = service.assignSite(assignmentInput, options.nowMs); + } + if (!assigned || assigned.success !== true) { + if (String(assigned && assigned.errorMsg || "") === "INDUSTRIAL_MINING_CREW_SECURITY_RESTRICTED") { + return { + success: false, + errorMsg: "X_COMMAND_CREW_SECURITY_RESTRICTED", + message: "That crew tier is not permitted in the selected system's security class.", + }; + } + return { + success: false, + errorMsg: "X_COMMAND_SITE_ASSIGNMENT_REJECTED", + message: `The mining site assignment was rejected (${String(assigned && assigned.errorMsg || "unknown error")}).`, + }; + } + if (assigned.replayed === true) { + return { + success: true, + message: "That site-assignment command was already processed; the current assignment is unchanged.", + data: listForCharacter(characterID, { + ...options, + session, + contractService: service, + siteCatalog, + }).data, + }; + } + const bookmarkResult = syncContractBookmark(assigned.data, { + bookmarkService: options.bookmarkService, + config: activeConfig, + }); + const bookmarkMessage = formatBookmarkMessage(bookmarkResult); + const materialized = runtime.applyOrder(session, assigned.data, { nowMs: options.nowMs }); + if (!materialized || materialized.success !== true) { + return { + success: false, + errorMsg: "X_COMMAND_SITE_DEPLOYMENT_FAILED", + message: + `The belt assignment was saved, but the miner could not deploy (${String(materialized && materialized.errorMsg || "unknown error")}).` + + (bookmarkMessage ? ` ${bookmarkMessage}` : ""), + data: listForCharacter(characterID, { + ...options, + session, + contractService: service, + siteCatalog, + }).data, + }; + } + const siteName = String( + validation.data && validation.data.siteDescription && validation.data.siteDescription.siteName || + `site ${siteID}`, + ); + const stateChanged = Boolean(materialized.data && materialized.data.stateChanged === true); + const deferred = Boolean(materialized.data && materialized.data.deferred === true); + let deploymentMessage = ""; + if (stateChanged) { + deploymentMessage = String( + materialized.data && materialized.data.message || + "The contract changed state before deployment; no replacement ship was deployed.", + ); + } else if (deferred) { + deploymentMessage = + `${String( + assigned.data.crewName || + assigned.data.pilotName || + assigned.data.contractID, + )} is assigned to ${siteName}. ` + + `The ${isMiningCrew ? "complete crew" : "mining ship"} will deploy when a capsuleer enters that system.`; + } else { + deploymentMessage = + `${String( + assigned.data.crewName || + assigned.data.pilotName || + assigned.data.contractID, + )} is deployed at ${siteName}.`; + } + return { + success: true, + message: deploymentMessage + (bookmarkMessage ? ` ${bookmarkMessage}` : ""), + data: listForCharacter(characterID, { + ...options, + session, + contractService: service, + siteCatalog, + }).data, + }; +} + +function performAction(characterID, action, input = {}, options = {}) { + const activeConfig = options.config || config; + if ( + activeConfig.xCommandIndustrialEnabled === false || + activeConfig.livingUniverseEnabled !== true || + activeConfig.industrialHirelingsEnabled !== true + ) { + return { + success: false, + errorMsg: "X_COMMAND_INDUSTRIAL_DISABLED", + message: "Industrial controls are disabled in X Command on this server.", + }; + } + const normalizedAction = String(action || "").trim().toLowerCase(); + if (normalizedAction === "hire") { + return hireIndustrialContract(characterID, input, options); + } + const selector = String(input.selector || input.contractID || "").trim(); + if (!selector || !/^[A-Za-z0-9_-]{1,96}$/.test(selector)) { + return { success: false, errorMsg: "X_COMMAND_CONTRACT_REQUIRED", message: "Choose a valid hireling contract." }; + } + if (normalizedAction === "assign_site") { + return assignMinerToSite(characterID, selector, input, options); + } + if (normalizedAction === "order") { + const order = String(input.order || "").trim().toLowerCase(); + if (!ORDERS.has(order)) { + return { success: false, errorMsg: "X_COMMAND_ORDER_INVALID", message: "Choose mine, haul, standby, home, or withdraw." }; + } + return runCommand(characterID, `order ${selector} ${order}`, options); + } + if (normalizedAction === "destination") { + const destination = String(input.destination || "").trim().toLowerCase(); + if (!/^(home|current|[1-9][0-9]{0,17})$/.test(destination)) { + return { success: false, errorMsg: "X_COMMAND_DESTINATION_INVALID", message: "Choose home, current, or a station ID." }; + } + return runCommand( + characterID, + `destination ${selector} ${destination}`, + { + ...options, + commandID: input.commandID, + }, + ); + } + if (SIMPLE_ACTIONS[normalizedAction]) { + return runCommand(characterID, `${SIMPLE_ACTIONS[normalizedAction]} ${selector}`, options); + } + return { success: false, errorMsg: "X_COMMAND_ACTION_INVALID", message: "That industrial action is not available." }; +} + +module.exports = { + assignMinerToSite, + buildOperationalSystems, + getCharacterSession, + getSessionLocation, + hireIndustrialContract, + listForCharacter, + performAction, + resolveContractPhase, + sanitizeCrewMember, + sanitizeCrewPackage, + sanitizeContract, +}; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandRegistry.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandRegistry.js new file mode 100644 index 0000000..694e709 --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandRegistry.js @@ -0,0 +1,71 @@ +"use strict"; + +const path = require("path"); +const config = require(path.join(__dirname, "../../config")); + +const MODULES = Object.freeze([ + { id: "overview", label: "Operations", capability: "summary" }, + { id: "contracts", label: "Contracts", capability: "planned" }, + { id: "universe", label: "Universe Intel", capability: "planned" }, + { id: "industrial", label: "Industrial Crews", capability: "control" }, + { id: "mercenaries", label: "Mercenary Outfits", capability: "planned" }, + { id: "production", label: "Production", capability: "planned" }, + { id: "logistics", label: "Logistics", capability: "partial" }, + { id: "salvage", label: "Salvage & Recovery", capability: "planned" }, + { id: "settings", label: "Configuration", capability: "read_only" }, +]); + +function buildModuleManifest(sourceConfig = config) { + const bridgeEnabled = sourceConfig.xCommandEnabled === true; + const industrialEnabled = bridgeEnabled && + sourceConfig.xCommandIndustrialEnabled !== false && + sourceConfig.livingUniverseEnabled === true && + sourceConfig.industrialHirelingsEnabled === true; + return MODULES.map((module) => { + if (module.id === "industrial") { + return { + ...module, + enabled: industrialEnabled, + state: industrialEnabled ? "connected" : "disabled", + reason: industrialEnabled + ? null + : "Requires X Command, Living Universe, and Industrial Hirelings master switches.", + }; + } + if (module.id === "logistics") { + return { + ...module, + enabled: industrialEnabled, + state: industrialEnabled ? "partial" : "disabled", + reason: industrialEnabled + ? "Industrial hauler orders are connected; general logistics contracts are not yet implemented." + : "Industrial hauling is disabled.", + }; + } + if (module.id === "universe") { + return { + ...module, + capability: "map_read", + enabled: bridgeEnabled, + state: bridgeEnabled ? "connected" : "disabled", + reason: bridgeEnabled + ? "Character-scoped topology, routing, visits, and owned industrial operations are available." + : "X Command is disabled.", + }; + } + if (["overview", "settings"].includes(module.id)) { + return { ...module, enabled: bridgeEnabled, state: bridgeEnabled ? "connected" : "disabled", reason: null }; + } + return { + ...module, + enabled: false, + state: "planned", + reason: "The interface is reserved; no authoritative EveJS adapter is installed yet.", + }; + }); +} + +module.exports = { + MODULES, + buildModuleManifest, +}; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandUniverseAdapter.js b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandUniverseAdapter.js new file mode 100644 index 0000000..a255fa6 --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/files/server/src/services/xCommand/xCommandUniverseAdapter.js @@ -0,0 +1,403 @@ +"use strict"; + +const crypto = require("crypto"); +const path = require("path"); + +const worldData = require(path.join(__dirname, "../../space/worldData")); +const marketTopology = require(path.join(__dirname, "../market/marketTopology")); +const mapTelemetryState = require(path.join(__dirname, "../map/mapTelemetryState")); +const sessionRegistry = require(path.join(__dirname, "../chat/sessionRegistry")); +const { + getDefaultContractService, +} = require(path.join( + __dirname, + "../industrialHirelings/industrialHirelingContracts", +)); +const { + getDefaultIndustrialMiningSiteCatalog, +} = require(path.join( + __dirname, + "../industrialHirelings/industrialHirelingSites", +)); +const { + getSessionLocation, + sanitizeContract, +} = require(path.join(__dirname, "./xCommandIndustrialAdapter")); + +const TOPOLOGY_SCHEMA_VERSION = 1; + +function toPositiveInt(value, fallback = 0) { + const numeric = Math.trunc(Number(value) || 0); + return numeric > 0 ? numeric : fallback; +} + +function toFiniteNumber(value, fallback = 0) { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : fallback; +} + +function deepFreeze(value) { + if (!value || typeof value !== "object" || Object.isFrozen(value)) { + return value; + } + for (const child of Object.values(value)) { + deepFreeze(child); + } + return Object.freeze(value); +} + +function sanitizeSolarSystem(systemArg) { + const system = systemArg && typeof systemArg === "object" ? systemArg : {}; + const solarSystemID = toPositiveInt(system.solarSystemID, 0); + if (!solarSystemID) return null; + const position = system.position && typeof system.position === "object" + ? system.position + : {}; + return { + solarSystemID, + solarSystemName: String(system.solarSystemName || `System ${solarSystemID}`), + constellationID: toPositiveInt(system.constellationID, 0), + regionID: toPositiveInt(system.regionID, 0), + factionID: toPositiveInt(system.factionID, 0), + security: toFiniteNumber(system.security, 0), + securityClass: String(system.securityClass || ""), + position: { + x: toFiniteNumber(position.x, 0), + y: toFiniteNumber(position.y, 0), + z: toFiniteNumber(position.z, 0), + }, + }; +} + +function sanitizeStation(stationArg) { + const station = stationArg && typeof stationArg === "object" ? stationArg : {}; + const stationID = toPositiveInt(station.stationID, 0); + const solarSystemID = toPositiveInt(station.solarSystemID, 0); + if (!stationID || !solarSystemID) return null; + return { + stationID, + stationName: String(station.stationName || `Station ${stationID}`), + solarSystemID, + constellationID: toPositiveInt(station.constellationID, 0), + regionID: toPositiveInt(station.regionID, 0), + stationTypeID: toPositiveInt(station.stationTypeID, 0), + }; +} + +function buildStaticTopology(sourceWorldData = worldData) { + const loadedSystemRows = sourceWorldData.getSolarSystems(); + const systemRows = Array.isArray(loadedSystemRows) ? loadedSystemRows : []; + const sanitizedSystems = systemRows + .map(sanitizeSolarSystem) + .filter(Boolean) + .sort((left, right) => left.solarSystemID - right.solarSystemID); + const knownSystemIDs = new Set( + sanitizedSystems.map((system) => system.solarSystemID), + ); + const connectedSystemIDs = new Set(); + const connectionsByKey = new Map(); + + for (const system of sanitizedSystems) { + const gates = sourceWorldData.getStargatesForSystem(system.solarSystemID); + for (const gateArg of Array.isArray(gates) ? gates : []) { + const destinationSolarSystemID = toPositiveInt( + gateArg && gateArg.destinationSolarSystemID, + 0, + ); + if ( + !destinationSolarSystemID || + destinationSolarSystemID === system.solarSystemID || + !knownSystemIDs.has(destinationSolarSystemID) + ) { + continue; + } + const fromSolarSystemID = Math.min( + system.solarSystemID, + destinationSolarSystemID, + ); + const toSolarSystemID = Math.max( + system.solarSystemID, + destinationSolarSystemID, + ); + const key = `${fromSolarSystemID}:${toSolarSystemID}`; + const gateID = toPositiveInt(gateArg && gateArg.itemID, 0); + const destinationGateID = toPositiveInt( + gateArg && gateArg.destinationID, + 0, + ); + const candidate = { + fromSolarSystemID, + toSolarSystemID, + fromStargateID: system.solarSystemID === fromSolarSystemID + ? gateID + : destinationGateID, + toStargateID: system.solarSystemID === fromSolarSystemID + ? destinationGateID + : gateID, + }; + const existing = connectionsByKey.get(key); + if (!existing) { + connectionsByKey.set(key, candidate); + } else { + if (!existing.fromStargateID && candidate.fromStargateID) { + existing.fromStargateID = candidate.fromStargateID; + } + if (!existing.toStargateID && candidate.toStargateID) { + existing.toStargateID = candidate.toStargateID; + } + } + connectedSystemIDs.add(system.solarSystemID); + connectedSystemIDs.add(destinationSolarSystemID); + } + } + + const systems = sanitizedSystems.map((system) => ({ + ...system, + hasStargateConnection: connectedSystemIDs.has(system.solarSystemID), + })); + const connections = [...connectionsByKey.values()] + .sort((left, right) => ( + left.fromSolarSystemID - right.fromSolarSystemID || + left.toSolarSystemID - right.toSolarSystemID + )); + const stationRows = typeof sourceWorldData.getStations === "function" + ? sourceWorldData.getStations() + : []; + const stations = (Array.isArray(stationRows) ? stationRows : []) + .map(sanitizeStation) + .filter((station) => station && knownSystemIDs.has(station.solarSystemID)) + .sort((left, right) => left.stationID - right.stationID); + const topologyBody = { + systems, + connections, + stations, + }; + const topologyVersion = crypto + .createHash("sha256") + .update(JSON.stringify(topologyBody), "utf8") + .digest("hex") + .slice(0, 16); + + return deepFreeze({ + schemaVersion: TOPOLOGY_SCHEMA_VERSION, + topologyVersion, + ...topologyBody, + counts: { + systems: systems.length, + connectedSystems: connectedSystemIDs.size, + connections: connections.length, + stations: stations.length, + }, + }); +} + +function sanitizeVisitRow(rowArg, sourceWorldData, telemetryState) { + const row = Array.isArray(rowArg) ? rowArg : []; + const solarSystemID = toPositiveInt(row[1], 0); + const visits = toPositiveInt(row[2], 0); + if (!solarSystemID || !visits) return null; + const system = sourceWorldData.getSolarSystemByID(solarSystemID); + return { + solarSystemID, + solarSystemName: String( + system && system.solarSystemName || + `System ${solarSystemID}`, + ), + visits, + lastVisitedAtMs: Math.max( + 0, + Math.trunc(telemetryState.msFromFiletime(row[0], 0) || 0), + ), + }; +} + +function sanitizeVisitEntry(entryArg, sourceWorldData, telemetryState) { + const entry = entryArg && typeof entryArg === "object" ? entryArg : {}; + const solarSystemID = toPositiveInt(entry.solarSystemID, 0); + const visits = toPositiveInt(entry.visits, 0); + if (!solarSystemID || !visits) return null; + const system = sourceWorldData.getSolarSystemByID(solarSystemID); + const lastVisitedAtMs = Math.max( + 0, + Math.trunc( + Number(entry.lastDateTimeMs) || + telemetryState.msFromFiletime(entry.lastDateTime, 0) || + 0, + ), + ); + return { + solarSystemID, + solarSystemName: String( + system && system.solarSystemName || + `System ${solarSystemID}`, + ), + visits, + lastVisitedAtMs, + }; +} + +function createUniverseAdapter(options = {}) { + const sourceWorldData = options.worldData || worldData; + const topologyService = options.marketTopology || marketTopology; + const telemetryState = options.mapTelemetryState || mapTelemetryState; + const findSessionByCharacterID = options.findSessionByCharacterID || + sessionRegistry.findSessionByCharacterID; + const now = typeof options.now === "function" ? options.now : Date.now; + let topologyCache = null; + + function getTopology() { + if (!topologyCache) { + topologyCache = buildStaticTopology(sourceWorldData); + } + return { success: true, data: topologyCache }; + } + + function getRoute(fromSolarSystemIDArg, toSolarSystemIDArg) { + const fromSolarSystemID = toPositiveInt(fromSolarSystemIDArg, 0); + const toSolarSystemID = toPositiveInt(toSolarSystemIDArg, 0); + if (!fromSolarSystemID || !toSolarSystemID) { + return { + success: false, + errorMsg: "X_COMMAND_UNIVERSE_ROUTE_SYSTEM_REQUIRED", + message: "Choose valid origin and destination solar systems.", + }; + } + const fromSystem = sourceWorldData.getSolarSystemByID(fromSolarSystemID); + const toSystem = sourceWorldData.getSolarSystemByID(toSolarSystemID); + if (!fromSystem || !toSystem) { + return { + success: false, + errorMsg: "X_COMMAND_UNIVERSE_SYSTEM_NOT_FOUND", + message: "The selected solar system does not exist.", + }; + } + const systemIDs = topologyService.getShortestPath( + fromSolarSystemID, + toSolarSystemID, + ); + const reachable = Array.isArray(systemIDs) && systemIDs.length > 0; + const systems = reachable + ? systemIDs + .map((solarSystemID) => sanitizeSolarSystem( + sourceWorldData.getSolarSystemByID(solarSystemID), + )) + .filter(Boolean) + : []; + return { + success: true, + data: { + fromSolarSystemID, + toSolarSystemID, + reachable, + jumps: reachable ? Math.max(0, systemIDs.length - 1) : null, + systemIDs: reachable ? [...systemIDs] : [], + systems, + }, + }; + } + + function getSnapshot(characterIDArg, context = {}) { + const characterID = toPositiveInt(characterIDArg, 0); + if (!characterID) { + return { + success: false, + errorMsg: "X_COMMAND_CHARACTER_REQUIRED", + message: "A linked character is required.", + }; + } + const session = Object.prototype.hasOwnProperty.call(context, "session") + ? context.session + : findSessionByCharacterID(characterID, { indexedOnly: true }); + const authRecord = context.authRecord && typeof context.authRecord === "object" + ? context.authRecord + : {}; + const location = getSessionLocation(session); + const currentSystem = location.solarSystemID > 0 + ? sourceWorldData.getSolarSystemByID(location.solarSystemID) + : null; + const hasScopedVisitReader = + typeof telemetryState.listSolarSystemVisits === "function"; + const visitEntries = hasScopedVisitReader + ? telemetryState.listSolarSystemVisits(characterID) + : telemetryState.listSolarSystemVisitRows(characterID); + const visits = (Array.isArray(visitEntries) ? visitEntries : []) + .map((entry) => ( + hasScopedVisitReader + ? sanitizeVisitEntry(entry, sourceWorldData, telemetryState) + : sanitizeVisitRow(entry, sourceWorldData, telemetryState) + )) + .filter(Boolean); + + const contractService = options.contractService || getDefaultContractService(); + const listed = contractService.listForCharacter(characterID); + if (!listed || listed.success !== true) { + return listed || { + success: false, + errorMsg: "X_COMMAND_UNIVERSE_CONTRACTS_UNAVAILABLE", + message: "Owned industrial operations are unavailable.", + }; + } + const siteCatalog = options.siteCatalog || + getDefaultIndustrialMiningSiteCatalog(); + const contracts = (Array.isArray(listed.data && listed.data.contracts) + ? listed.data.contracts + : []) + .filter((contract) => ( + toPositiveInt(contract && contract.ownerCharacterID, 0) === characterID + )) + .map((contract) => sanitizeContract(contract, { siteCatalog })) + .filter((contract) => contract && contract.contractID); + + return { + success: true, + data: { + schemaVersion: 1, + character: { + characterID, + characterName: String( + session && (session.characterName || session.charname) || + authRecord.characterName || + `Character ${characterID}`, + ), + ...location, + solarSystemName: String( + currentSystem && currentSystem.solarSystemName || + "", + ), + }, + visits, + industrial: { + enabled: listed.data && listed.data.enabled === true, + contracts, + }, + serverTimeMs: Math.max(0, Math.trunc(Number(now()) || Date.now())), + }, + }; + } + + return Object.freeze({ + getRoute, + getSnapshot, + getTopology, + }); +} + +let defaultAdapter = null; +function getDefaultUniverseAdapter() { + if (!defaultAdapter) { + defaultAdapter = createUniverseAdapter(); + } + return defaultAdapter; +} + +module.exports = { + TOPOLOGY_SCHEMA_VERSION, + buildStaticTopology, + createUniverseAdapter, + deepFreeze, + getDefaultUniverseAdapter, + sanitizeSolarSystem, + sanitizeStation, + sanitizeVisitEntry, + sanitizeVisitRow, +}; diff --git a/bridge/v0.2.2-evejs-v0.12.3.1/manifest.json b/bridge/v0.2.2-evejs-v0.12.3.1/manifest.json new file mode 100644 index 0000000..c22f939 --- /dev/null +++ b/bridge/v0.2.2-evejs-v0.12.3.1/manifest.json @@ -0,0 +1,56 @@ +{ + "schemaVersion": 1, + "overlayVersion": "0.2.0", + "targetRelease": "X-Eve Living Universe v0.2.2", + "targetEveJsVersion": "v0.12.3.1", + "targetRepository": "https://github.com/kaiwren986/evejs-living-universe", + "targetTag": "v0.2.2", + "sharedFiles": [ + { + "path": "server/src/config/index.js", + "baselineSha256": "80E2AF44DC5A3F6C394A72D8A0D2703387B47D46F73788FD7D85E6492DE696DC", + "installedSha256": "7C590BA01424173FB23439048FBB8C3DBB594AC195D90C639A1A95A7B9713D85" + }, + { + "path": "server/src/services/chat/chatCommands.js", + "baselineSha256": "A8DD41BCEC7B1ED17798AE606C2398F158D3C98D1A284E755D7F8B3578C0C3C5", + "installedSha256": "1BC9582D3AA9CAFA4B0B6962A53D10188EADB51DD8F102057BCF40B8B913C786" + }, + { + "path": "server/src/gameStore/index.js", + "baselineSha256": "4E91711AA57B2A13B4648ABFCD9809C4092AFAFAB10165095FB3176F906D44FB", + "installedSha256": "BCD2E33648B255F1CD63A92B796BB8A22D503DBB7ADD07BB6A84A02231C3D169" + }, + { + "path": "server/src/gameStore/sqliteStore.js", + "baselineSha256": "E57D6E836822C2FCC9FDC6BBFF94948B07E315546431A00708C935D0B4FA52C0", + "installedSha256": "A5704E0617EA5E4626A89B944509EF23F2C85054460B000D7266117217E3EE98" + }, + { + "path": "server/src/gameStore/tableOwnership.js", + "baselineSha256": "6694FD181C66A9CE2E7DF93B1C24CFAEF64E49353112AA2FFC60029F35A13966", + "installedSha256": "0401CE5E133D9C3DCD6A5D1136CA056C4C089931CA716D5AABA75F2A0BEAC27C" + }, + { + "path": "compose.yaml", + "baselineSha256": "7492471139A278FAB3D45378240986C957B17DDBA602DE2DBDC4DC27DBBC1A99", + "installedSha256": "3CA145CB0EA10B3F336A48D6ECA8FA1DD2C8AADE936F87C1D543FED2466E08EF" + } + ], + "addedFiles": [ + { "path": "server/assets/x-command/assets/index-CBiB6eNy.css", "sha256": "60B721E9AA1BAAAE68E5BE980D6A00FF9A0543673A7257B26F89E1BE1ACFBB75" }, + { "path": "server/assets/x-command/assets/index-CG2pp9Po.js", "sha256": "E79CF187A3452E0BB9E11A603110E740C49125265BC3A10FEBFC523EE0BC570C" }, + { "path": "server/assets/x-command/index.html", "sha256": "5A146ED99A3053E183A94869D0D946529A1D0AF058744CCF0021CEF56325150C" }, + { "path": "server/scripts/verifyXCommandBridge.js", "sha256": "D09CA38077963A8F09658C8E04A0F5B1AC2945B59761F6FF9796BF0F94B51468" }, + { "path": "server/scripts/verifyXCommandLocalUi.js", "sha256": "8D162E67D2E402249B2BCC4720270132CC9657908717ED98325D1DC3391ED856" }, + { "path": "server/scripts/verifyXCommandUniverse.js", "sha256": "82A653ABC1CDD9F61889A92074EA0B336EDB693E0EA26AF245489CE31FB80940" }, + { "path": "server/src/_secondary/xCommand/httpAuth.js", "sha256": "5E8B7958CC4E07DE5CDE857B50031071F7DC9BFE859F4838C41C6FCEEBEE3904" }, + { "path": "server/src/_secondary/xCommand/server.js", "sha256": "72BFF63FB8A7ED05E73E2BF96CDE7FD051CDFD150BB21125BCEFB959012060DA" }, + { "path": "server/src/_secondary/xCommand/staticUi.js", "sha256": "7D445476BA1665E8343CA9A440070654631A7F5D8F2CA2F8AF7F7E3A7C6C5194" }, + { "path": "server/src/services/xCommand/xCommandAuth.js", "sha256": "9C16C561CA783E40ACEDCC5CF139998BCF8FC5DEB5E6C7F2E9DF9BC67935A002" }, + { "path": "server/src/services/xCommand/xCommandChatCommand.js", "sha256": "C811C9EC89090CF0F2F704E2826F3F4D2394F196C6142FB865429FE35A140E33" }, + { "path": "server/src/services/xCommand/xCommandIndustrialAdapter.js", "sha256": "54ACBDB45D6CB58F5549D6499A87D28D437A77E14E6287493DAD563720C578E3" }, + { "path": "server/src/services/xCommand/xCommandRegistry.js", "sha256": "C157102BF42AD575FE564C8D80523F736C5DA586C6F55D33F53F0DF64CFC9A41" }, + { "path": "server/src/services/xCommand/xCommandUniverseAdapter.js", "sha256": "E9C5BC76E0DCF8882BCB9C3A6B1E723B16113E5240548F67B60079CC30B5D5C9" } + ] +} diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 4d564ba..0c53069 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -67,6 +67,19 @@ association and never commit its project ID. ## Server work -Read [EveJS integration](INTEGRATION.md) before changing the reference server -code. The files in `server-reference/` are intentionally isolated so nobody -can mistake them for a complete EveJS distribution or a current installer. +Read [EveJS integration](INTEGRATION.md) before changing server code. The +installable payload is under `bridge/v0.2.2-evejs-v0.12.3.1/files/`; the +`server-reference/` directory is historical. + +When the bridge or frontend bundle changes: + +1. run `npm run build:evejs`; +2. replace the packaged `server/assets/x-command/` payload; +3. update every packaged SHA-256 in `manifest.json`; +4. install onto a fresh copy of the exact public v0.2.2 release; +5. run all focused verifiers and listener/API/static-UI smoke tests; +6. uninstall and prove every shared baseline hash is restored; and +7. run `npm run check` before opening the pull request. + +Do not relax the baseline hashes to make a modified target install. Add and +test a new versioned overlay when the public Living Universe source changes. diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index ddf3c27..07d148a 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -1,15 +1,27 @@ # EveJS integration -## Compatibility warning +## Supported overlay -The current public X-Eve Living Universe v0.2.2 release does not include X -Command. Do not copy the reference files over a play server and do not apply -the v0.12.2 patch archive to v0.12.3.1. +X Command v0.2.0 includes an exact-version overlay for public X-Eve Living +Universe v0.2.2 / EveJS v0.12.3.1. Stop the server, then install from the +repository root: + +```bat +Install-Server-Bridge.bat "C:\path\to\your\LivingUniverse" +``` + +The installer rejects an unsupported or locally modified source baseline, +backs up shared files, installs the current bridge and UI, enables native and +Docker loopback use, and runs the three focused bridge verifiers. It can be +run again safely to verify an existing install. `server-reference/v0.12.3.1/` contains only the original X Command modules and -focused verifiers from the last known-good v0.12.3.1 integration workspace. -It omits complete upstream/shared files and is not runnable by itself. It is a -porting reference for a future overlay. +focused verifiers from the earlier integration workspace. It remains a +historical source reference; use `bridge/v0.2.2-evejs-v0.12.3.1/` for the +current installer. + +Do not apply the v0.12.2 archive or manually copy either reference directory +into a play server. ## Intended runtime @@ -55,9 +67,9 @@ system: The two public URLs should resolve to the same origin with the current UI. Local-only development may use `http://127.0.0.1:26500/`. -## Required shared wiring +## Installed shared wiring -A complete port needs more than the files under `server-reference/`: +The overlay adds only these integration hooks to the public v0.2.2 tree: 1. Register the X Command secondary service so EveJS starts and stops it with the rest of the server stack. @@ -66,13 +78,27 @@ A complete port needs more than the files under `server-reference/`: 3. Register `xCommandAuth` with the game-store index, SQLite store, and table ownership map. 4. Route `/x`, `/x status`, and `/x reset` through the chat command service. -5. Add verifier scripts to `server/package.json`. +5. Install the three focused verifier scripts. 6. Build this repository with `npm run build:evejs` and package the output as `server/assets/x-command/`. -7. Preserve current public startup/readiness behavior: port 26500 is optional +7. Add Docker environment and loopback port mapping for 26500. +8. Preserve current public startup/readiness behavior: port 26500 is optional unless X Command is enabled, and X Command telemetry must not cause Play to start a duplicate server. +For Docker, rebuild the image after install because Compose does not +bind-mount server source into an existing image: + +```powershell +docker compose build init +docker compose up --detach +``` + +The bridge is enabled by `evejs.config.x-eve.local.json` in native mode and +by Compose environment variables in Docker. Port 26500 is never added to the +Play launcher readiness decision; the cross-mode contract remains game port +26000 plus proxy port 26002. + ## Security invariants - One-time link tokens are character-bound, expire quickly, and are stored @@ -86,17 +112,21 @@ A complete port needs more than the files under `server-reference/`: filesystem data. - Logout and `/x reset` revoke the associated session. -## Porting gate +## Verification and release gate -At minimum, a future overlay should pass: +The current package passes frontend checks, overlay integrity checks, the +three focused server verifiers, clean-copy installation, isolated listener +and HTTP smoke checks, and exact rollback. Before calling the overlay stable, +it should also pass: - the current Living Universe repository audit and all bundled verifiers; - the three X Command server verifiers included here; - this repository's `npm run check`; -- a clean baseline installation and rollback test; - first start, second start, native and Docker readiness checks; - browser link, expiry, logout, cross-origin rejection, and multi-character isolation tests; and - a sustained runtime soak with the X Command listener enabled. -Only after that should the overlay be described as compatible or installable. +Until those runtime gates complete, releases containing the overlay remain +marked prerelease even though the installer itself is supported for the exact +v0.2.2 target. diff --git a/docs/STATUS.md b/docs/STATUS.md index 9fee00c..36dc792 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -4,11 +4,15 @@ Last reviewed: 2026-07-31. ## Short version -The frontend is healthy and reproducible. The last v0.12.3.1 server -implementation passed its three focused verifiers on 2026-07-26, but it was -curated out of the public Living Universe package and has not been packaged -as a current overlay. Therefore the repository is ready for interface -development, but it is not yet a zero-friction live-server release. +The frontend is healthy and reproducible. X Command v0.2.0 now includes an +exact-version server-bridge overlay for public X-Eve Living Universe v0.2.2 / +EveJS v0.12.3.1. A clean-copy install, all three focused bridge verifiers, an +isolated live listener/API/static-UI smoke test, idempotent install, and exact +rollback passed on 2026-07-31. + +The overlay is usable for current public development and local testing. It is +still a prerelease because the combined package has not completed a full +native and Docker game-session gate or a sustained post-integration soak. ## Verified now @@ -17,6 +21,7 @@ These checks pass from a clean dependency install: - `npm run lint` - `npm test` - `npm run build:evejs` +- `npm run check:bridge` `npm test` performs a production vinext build and runs 20 tests covering: @@ -30,24 +35,32 @@ These checks pass from a clean dependency install: commands; and - accessibility and responsive source contracts. -The preserved v0.12.3.1 integration workspace also recorded passes for: +The v0.2.0 overlay installed over an untouched public v0.2.2 tree and passed: - `verifyXCommandBridge.js` - `verifyXCommandLocalUi.js` - `verifyXCommandUniverse.js` - -Those server results establish that the reference code worked in that exact -integration tree. They do not prove compatibility with later public patches. +- JavaScript syntax checks for the patched shared files; +- listener health on `127.0.0.1:26500`; +- static `GET /` with HTTP 200; +- unauthenticated `GET /api/v1/session` rejection with HTTP 401; +- Docker Compose parse and bridge environment/port assertions; and +- uninstall followed by SHA-256 equality for all six restored shared files, + with zero bridge-owned files remaining. + +The isolated runtime smoke used the public source tree without a full game +database, so missing-reference warnings in that test are expected and are not +evidence of a full EveJS startup pass. ## Feature matrix | Screen or behavior | Preview mode | Live adapter contract | Current public server | | --- | --- | --- | --- | -| Operations | Sample dashboard | Industrial totals and activity | Bridge absent | -| Industrial Crews | Sample miner/hauler | Contracts, packages, sites, cargo, lifecycle controls | Bridge absent | -| Universe Map | Sample topology and route | Topology, owned-operation snapshot, route, destination order | Bridge absent | -| Connection & Modules | Preview flags | Character session and module manifest | Bridge absent | -| Logistics | Sample presentation | Industrial hauler subset only | Bridge absent | +| Operations | Sample dashboard | Industrial totals and activity | Available after overlay install | +| Industrial Crews | Sample miner/hauler | Contracts, packages, sites, cargo, lifecycle controls | Available after overlay install | +| Universe Map | Sample topology and route | Topology, owned-operation snapshot, route, destination order | Available after overlay install | +| Connection & Modules | Preview flags | Character session and module manifest | Available after overlay install | +| Logistics | Sample presentation | Industrial hauler subset only | Available after overlay install | | Contracts | Sample presentation | No adapter | Not implemented | | Mercenary Outfits | Sample presentation | Deliberately unavailable | Not implemented | | Production | Sample presentation | No adapter | Not implemented | @@ -58,30 +71,33 @@ records is shown as unavailable rather than filled with fake live activity. ## Known limitations -1. **No current installer.** The public Living Universe v0.2.2 / EveJS - v0.12.3.1 tree does not contain the X Command listener, API endpoints, - static UI service, `/x` command, or auth-store wiring. -2. **Same-origin only.** The browser requires the UI and API to share an +1. **Exact target only.** The installer intentionally accepts only the public + Living Universe v0.2.2 source fingerprints. It refuses modified trees and + later releases until their shared hooks are reviewed and recorded. +2. **Prerelease runtime coverage.** Focused bridge tests and a live listener + smoke pass, but the overlay has not yet completed a real-client `/x` + session, full native/Docker first-and-second-start matrix, or sustained + combined soak on the public package. +3. **Docker rebuild required.** The overlay patches the host source and + Compose wiring; an already-built `evejs-local` image must be rebuilt after + install or uninstall. +4. **Same-origin only.** The browser requires the UI and API to share an origin. The older separate `command.example.com` plus `command-api.example.com` layout is no longer accepted by the frontend. -3. **No public hosted service.** The original private deployment association +5. **No public hosted service.** The original private deployment association is intentionally not published. `.openai/hosting.json` contains neutral values only. -4. **No site accounts.** Character identity comes from EveJS `/x` links. The +6. **No site accounts.** Character identity comes from EveJS `/x` links. The unused `app/chatgpt-auth.ts` helper remains from the site scaffold but is not connected to X Command. -5. **No application database.** X Command does not currently store player +7. **No application database.** X Command does not currently store player state in D1 or R2. Unused Drizzle example code was removed before public release. -6. **Economy depends on the server.** Crew prices are displayed as quote-only +8. **Economy depends on the server.** Crew prices are displayed as quote-only when the bridge reports economy disabled. The UI only claims settlement when the server reports economy enabled. -7. **Remote ship renders.** Ship portraits use the public EVE Image Server and +9. **Remote ship renders.** Ship portraits use the public EVE Image Server and require browser network access. -8. **No Docker packaging or cross-origin proxy recipe is maintained here.** -9. **No post-public-release end-to-end soak.** The frontend and preserved - server reference have focused verification, but the combined overlay has - not passed the current public release's clean-install and soak gates. ## Historical material @@ -90,14 +106,12 @@ hireling/X Command series. It is retained to preserve design history and earlier implementation details. It must not be applied to EveJS v0.12.3.1 or the public Living Universe release. -## What would make live mode releasable - -- Port the reference server modules onto the current public Living Universe - branch without replacing newer scheduler or startup fixes. -- Wire the shared config, auth table, `/x` chat route, secondary listener, and - package verifier scripts described in `INTEGRATION.md`. -- Build and embed the current `dist/evejs-client` output. -- Add an installer/rollback manifest that targets an exact public release. -- Pass clean install, first start, second start, Docker/native readiness, - bridge security, current Living Universe tests, and a sustained runtime - gate. +## What would promote the overlay from prerelease + +- Start an installed public game database natively and in Docker, twice each. +- Complete a real-client `/x` exchange, refresh, logout, reset, expiry, and + second-character isolation pass. +- Confirm Play still treats game port 26000 plus proxy port 26002 as ready and + never starts a second native server for a healthy Docker stack. +- Run the current Living Universe verification suite and a sustained runtime + soak with the optional listener enabled. diff --git a/eslint.config.mjs b/eslint.config.mjs index da44edc..7419707 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,7 @@ export default defineConfig([ ".wrangler/**", "dist/**", "out/**", + "bridge/**", "server-reference/**", "legacy-server/**", "next-env.d.ts", diff --git a/package-lock.json b/package-lock.json index 51af50a..0c19fb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "x-command", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "x-command", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "next": "16.2.12", diff --git a/package.json b/package.json index dd280b6..abb1bac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "x-command", - "version": "0.1.0", - "description": "Community-maintained X Command dashboard and EveJS integration reference.", + "version": "0.2.0", + "description": "Community-maintained X Command dashboard and installable X-Eve Living Universe server bridge.", "private": true, "license": "MIT", "repository": { @@ -22,7 +22,8 @@ "start": "vinext start", "test": "npm run build && node --test tests/rendered-html.test.mjs", "lint": "eslint . --ignore-pattern dist --ignore-pattern .next", - "check": "npm run lint && npm test && npm run build:evejs" + "check:bridge": "node tools/x-command-bridge.mjs check-package", + "check": "npm run lint && npm test && npm run build:evejs && npm run check:bridge" }, "dependencies": { "next": "16.2.12", diff --git a/server-reference/v0.12.3.1/README.md b/server-reference/v0.12.3.1/README.md index 6f383b9..5905513 100644 --- a/server-reference/v0.12.3.1/README.md +++ b/server-reference/v0.12.3.1/README.md @@ -20,5 +20,6 @@ topology/snapshot/route adapters. They depend on the Living Universe industrial services and normal EveJS configuration, game-store, chat, and secondary-service systems. -Use [the integration guide](../../docs/INTEGRATION.md) as the porting -checklist. Do not copy this directory directly into a play server. +Use [the current versioned overlay](../../bridge/v0.2.2-evejs-v0.12.3.1/) +for public Living Universe v0.2.2. Keep this directory only as a porting and +history reference, and do not copy it directly into a play server. diff --git a/tools/x-command-bridge.mjs b/tools/x-command-bridge.mjs new file mode 100644 index 0000000..7765857 --- /dev/null +++ b/tools/x-command-bridge.mjs @@ -0,0 +1,628 @@ +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import net from "node:net"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptPath = fileURLToPath(import.meta.url); +const repositoryRoot = path.resolve(path.dirname(scriptPath), ".."); +const bridgeRoot = path.join( + repositoryRoot, + "bridge", + "v0.2.2-evejs-v0.12.3.1", +); +const manifestPath = path.join(bridgeRoot, "manifest.json"); + +function parseArguments(argv) { + const [command = "help", ...rest] = argv; + const options = { command, force: false, skipTests: false, target: "" }; + for (let index = 0; index < rest.length; index += 1) { + const argument = rest[index]; + if (argument === "--target") { + options.target = rest[index + 1] || ""; + index += 1; + } else if (argument === "--force") { + options.force = true; + } else if (argument === "--skip-tests") { + options.skipTests = true; + } else if (!argument.startsWith("-") && !options.target) { + options.target = argument; + } else { + throw new Error(`Unknown argument: ${argument}`); + } + } + return options; +} + +function relativePath(value) { + return String(value).replaceAll("/", path.sep); +} + +async function exists(filePath) { + try { + await stat(filePath); + return true; + } catch (error) { + if (error && error.code === "ENOENT") return false; + throw error; + } +} + +async function sha256(filePath) { + const bytes = await readFile(filePath); + return createHash("sha256").update(bytes).digest("hex").toUpperCase(); +} + +async function loadManifest() { + return JSON.parse(await readFile(manifestPath, "utf8")); +} + +function resolveTarget(value) { + if (!value) { + throw new Error( + "A Living Universe install folder is required. Use --target C:\\path\\to\\LivingUniverse.", + ); + } + return path.resolve(value); +} + +function detectNewline(text) { + return text.includes("\r\n") ? "\r\n" : "\n"; +} + +function replaceOnce(text, beforeLines, afterLines, label) { + const newline = detectNewline(text); + const before = beforeLines.join(newline); + const after = afterLines.join(newline); + const first = text.indexOf(before); + if (first < 0) throw new Error(`Could not locate the ${label} insertion point.`); + if (text.indexOf(before, first + before.length) >= 0) { + throw new Error(`The ${label} insertion point is ambiguous.`); + } + return `${text.slice(0, first)}${after}${text.slice(first + before.length)}`; +} + +const xCommandConfigBlock = [ + " {", + ' key: "xCommandEnabled",', + " defaultValue: false,", + ' envVar: "EVEJS_X_COMMAND_ENABLED",', + ' envType: "boolean",', + " description: [", + ' "Master switch for the optional X Command web bridge.",', + ' "The bridge is request-driven and never runs work inside the space tick.",', + " ],", + ' validValues: "true or false.",', + " },", + " {", + ' key: "xCommandApiBindHost",', + ' defaultValue: "127.0.0.1",', + ' envVar: "EVEJS_X_COMMAND_API_BIND_HOST",', + ' envType: "string",', + " description:", + ' "Bind address for the X Command API. Keep loopback when a VPS reverse proxy provides public HTTPS.",', + ' validValues: \'Host name or IP string such as "127.0.0.1".\',', + " },", + " {", + ' key: "xCommandApiPort",', + " defaultValue: 26500,", + ' envVar: "EVEJS_X_COMMAND_API_PORT",', + ' envType: "number",', + " minValue: 1,", + " maxValue: 65535,", + ' description: "Loopback HTTP port used by the X Command API service.",', + ' validValues: "Integer TCP port from 1 through 65535.",', + " },", + " {", + ' key: "xCommandPublicUrl",', + ' defaultValue: "",', + ' envVar: "EVEJS_X_COMMAND_PUBLIC_URL",', + ' envType: "string",', + " description:", + ' "Public HTTPS URL of the X Command interface placed in the one-click in-game link.",', + ' validValues: \'Empty or an absolute HTTPS URL such as "https://command.example.com/".\',', + " },", + " {", + ' key: "xCommandApiPublicUrl",', + ' defaultValue: "",', + ' envVar: "EVEJS_X_COMMAND_API_PUBLIC_URL",', + ' envType: "string",', + " description:", + ' "Public HTTPS URL of the reverse-proxied X Command API embedded in character links.",', + ' validValues: \'Empty or an absolute HTTPS URL such as "https://command-api.example.com/".\',', + " },", + " {", + ' key: "xCommandAllowedOrigins",', + ' defaultValue: "",', + ' envVar: "EVEJS_X_COMMAND_ALLOWED_ORIGINS",', + ' envType: "string",', + " description:", + ' "Comma-separated browser origins allowed to call the X Command API. The configured interface origin is allowed automatically.",', + ' validValues: "Empty or a comma-separated list of absolute HTTP(S) origins.",', + " },", + " {", + ' key: "xCommandLinkMinutes",', + " defaultValue: 5,", + ' envVar: "EVEJS_X_COMMAND_LINK_MINUTES",', + ' envType: "number",', + " minValue: 1,", + " maxValue: 30,", + " description:", + ' "Lifetime of the one-time sign-in link created by /x. Opening it exchanges it for a browser session.",', + ' validValues: "Integer minutes from 1 through 30.",', + " },", + " {", + ' key: "xCommandSessionHours",', + " defaultValue: 720,", + ' envVar: "EVEJS_X_COMMAND_SESSION_HOURS",', + ' envType: "number",', + " minValue: 1,", + " maxValue: 8760,", + " description:", + ' "Lifetime of the browser session created after a one-time /x link is opened.",', + ' validValues: "Integer hours from 1 through 8760.",', + " },", + " {", + ' key: "xCommandIndustrialEnabled",', + " defaultValue: true,", + ' envVar: "EVEJS_X_COMMAND_INDUSTRIAL_ENABLED",', + ' envType: "boolean",', + " description:", + ' "Exposes the industrial hireling adapter through X Command when its own master switches are enabled.",', + ' validValues: "true or false.",', + " },", +]; + +const transforms = { + "server/src/config/index.js": (text) => replaceOnce( + text, + [" {", ' key: "liveEventsEnabled",'], + [...xCommandConfigBlock, " {", ' key: "liveEventsEnabled",'], + "configuration", + ), + "server/src/services/chat/chatCommands.js": (text) => { + let output = replaceOnce( + text, + [ + "const {", + " handleOrcaCommand: executeOrcaCommand,", + ], + [ + "const {", + " executeXCommandChatCommand,", + "} = require(path.join(", + " __dirname,", + ' "../xCommand/xCommandChatCommand",', + "));", + "const {", + " handleOrcaCommand: executeOrcaCommand,", + ], + "chat import", + ); + output = replaceOnce( + output, + [' "hireling",', ' "hirelings",', ' "help",'], + [' "hireling",', ' "hirelings",', ' "x",', ' "xcommand",', ' "help",'], + "chat command allowlist", + ); + return replaceOnce( + output, + [ + ' if (command === "dock") {', + " return handleHomeDock(session, chatHub, options);", + " }", + ], + [ + ' if (command === "x" || command === "xcommand") {', + " const result = executeXCommandChatCommand(session, argumentText);", + " return handledResult(chatHub, session, options, result.message);", + " }", + "", + ' if (command === "dock") {', + " return handleHomeDock(session, chatHub, options);", + " }", + ], + "chat dispatch", + ); + }, + "server/src/gameStore/index.js": (text) => { + let output = replaceOnce( + text, + [' "industrialHirelingContracts",', ' "xEveRuntime",'], + [' "industrialHirelingContracts",', ' "xCommandAuth",', ' "xEveRuntime",'], + "recoverable table", + ); + return replaceOnce( + output, + [' "industrialHirelingContracts",', ' "liveEventRuntime",'], + [' "industrialHirelingContracts",', ' "xCommandAuth",', ' "liveEventRuntime",'], + "SQLite table", + ); + }, + "server/src/gameStore/sqliteStore.js": (text) => replaceOnce( + text, + [' industrialHirelingContracts: ["contracts", "archive"],', ' liveEventRuntime: ["events", "transactions", "archive"],'], + [' industrialHirelingContracts: ["contracts", "archive"],', ' xCommandAuth: ["sessions", "characters"],', ' liveEventRuntime: ["events", "transactions", "archive"],'], + "SQLite row groups", + ), + "server/src/gameStore/tableOwnership.js": (text) => replaceOnce( + text, + [ + "define([\"corporationBills\"], {", + ' tier: TIERS.RUNTIME, domain: "service:account", confidence: CONFIDENCE.SCANNER,', + "});", + ], + [ + 'define(["xCommandAuth"], {', + " tier: TIERS.RUNTIME,", + ' domain: "service:xCommand",', + " confidence: CONFIDENCE.SCANNER,", + ' note: "Persistent hashed one-click browser sessions for the optional X Command bridge.",', + "});", + 'define(["corporationBills"], {', + ' tier: TIERS.RUNTIME, domain: "service:account", confidence: CONFIDENCE.SCANNER,', + "});", + ], + "table ownership", + ), + "compose.yaml": (text) => { + let output = replaceOnce( + text, + [" EVEJS_XMPP_SERVER_BIND_HOST: 0.0.0.0"], + [ + " EVEJS_XMPP_SERVER_BIND_HOST: 0.0.0.0", + ' EVEJS_X_COMMAND_ENABLED: "true"', + " EVEJS_X_COMMAND_API_BIND_HOST: 0.0.0.0", + ], + "Docker environment", + ); + return replaceOnce( + output, + [' - "127.0.0.1:26002:26002"'], + [' - "127.0.0.1:26002:26002"', ' - "127.0.0.1:26500:26500"'], + "Docker port", + ); + }, +}; + +async function isPortOpen(port) { + return new Promise((resolve) => { + const socket = net.createConnection({ host: "127.0.0.1", port }); + const finish = (value) => { + socket.destroy(); + resolve(value); + }; + socket.setTimeout(400); + socket.once("connect", () => finish(true)); + socket.once("timeout", () => finish(false)); + socket.once("error", () => finish(false)); + }); +} + +async function requireStoppedServer() { + const openPorts = []; + for (const port of [26000, 26500]) { + if (await isPortOpen(port)) openPorts.push(port); + } + if (openPorts.length > 0) { + throw new Error( + `Stop the EveJS server before changing the bridge. Listening port(s): ${openPorts.join(", ")}.`, + ); + } +} + +async function writeText(filePath, text) { + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, text, "utf8"); +} + +async function validateBaseline(targetRoot, manifest) { + for (const entry of manifest.sharedFiles) { + const targetPath = path.join(targetRoot, relativePath(entry.path)); + if (!await exists(targetPath)) throw new Error(`Target file is missing: ${entry.path}`); + const actual = await sha256(targetPath); + if (actual !== entry.baselineSha256) { + throw new Error( + `${entry.path} does not match the supported Living Universe v0.2.2 baseline. ` + + `Expected ${entry.baselineSha256}; found ${actual}.`, + ); + } + } + for (const entry of manifest.addedFiles) { + const targetPath = path.join(targetRoot, relativePath(entry.path)); + if (await exists(targetPath)) { + throw new Error(`The target already contains bridge file ${entry.path}.`); + } + } +} + +async function validateInstalledFiles(targetRoot, manifest) { + const failures = []; + for (const entry of [...manifest.sharedFiles, ...manifest.addedFiles]) { + const targetPath = path.join(targetRoot, relativePath(entry.path)); + if (!await exists(targetPath)) { + failures.push(`${entry.path}: missing`); + continue; + } + const expected = entry.installedSha256 || entry.sha256; + const actual = await sha256(targetPath); + if (expected && actual !== expected) { + failures.push(`${entry.path}: expected ${expected}, found ${actual}`); + } + } + const configPath = path.join(targetRoot, "evejs.config.x-eve.local.json"); + if (!await exists(configPath)) { + failures.push("evejs.config.x-eve.local.json: missing"); + } else { + try { + const config = JSON.parse(await readFile(configPath, "utf8")); + if (config.xCommandEnabled !== true) { + failures.push("evejs.config.x-eve.local.json: xCommandEnabled is not true"); + } + } catch (error) { + failures.push(`evejs.config.x-eve.local.json: ${error.message}`); + } + } + if (failures.length > 0) { + throw new Error(`Bridge verification failed:\n- ${failures.join("\n- ")}`); + } +} + +function runServerVerifiers(targetRoot) { + const scripts = [ + "server/scripts/verifyXCommandBridge.js", + "server/scripts/verifyXCommandUniverse.js", + "server/scripts/verifyXCommandLocalUi.js", + ]; + for (const script of scripts) { + const result = spawnSync(process.execPath, [relativePath(script)], { + cwd: targetRoot, + encoding: "utf8", + stdio: "pipe", + }); + if (result.stdout) process.stdout.write(result.stdout); + if (result.stderr) process.stderr.write(result.stderr); + if (result.status !== 0) { + throw new Error(`${script} failed with exit code ${result.status}.`); + } + } +} + +async function install(targetRoot, manifest, options) { + await requireStoppedServer(); + const stateRoot = path.join(targetRoot, "_local", "x-command"); + const statePath = path.join(stateRoot, "install-state.json"); + if (await exists(statePath)) { + await verify(targetRoot, manifest, options); + console.log("X Command server bridge is already installed and verified."); + return; + } + await validateBaseline(targetRoot, manifest); + + const installationID = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-"); + const backupRoot = path.join(stateRoot, "backups", installationID); + const localConfigPath = path.join(targetRoot, "evejs.config.x-eve.local.json"); + const originalConfigExists = await exists(localConfigPath); + const originalConfigText = originalConfigExists + ? await readFile(localConfigPath, "utf8") + : null; + const originalConfig = originalConfigExists + ? JSON.parse(originalConfigText) + : {}; + if (!originalConfig || Array.isArray(originalConfig) || typeof originalConfig !== "object") { + throw new Error("evejs.config.x-eve.local.json must contain a JSON object."); + } + const originalConfigHadXCommandEnabled = Object.hasOwn( + originalConfig, + "xCommandEnabled", + ); + const context = { + added: [], + backupRoot, + originalConfigExists, + originalConfigText, + shared: [], + }; + + try { + for (const entry of manifest.sharedFiles) { + const relative = relativePath(entry.path); + const targetPath = path.join(targetRoot, relative); + const backupPath = path.join(backupRoot, relative); + await mkdir(path.dirname(backupPath), { recursive: true }); + await copyFile(targetPath, backupPath); + context.shared.push(relative); + const transformer = transforms[entry.path]; + if (!transformer) throw new Error(`No transformer exists for ${entry.path}.`); + await writeText(targetPath, transformer(await readFile(targetPath, "utf8"))); + } + + if (originalConfigExists) { + await copyFile( + localConfigPath, + path.join(backupRoot, "evejs.config.x-eve.local.json"), + ); + } + + for (const entry of manifest.addedFiles) { + const relative = relativePath(entry.path); + const sourcePath = path.join(bridgeRoot, "files", relative); + const targetPath = path.join(targetRoot, relative); + await mkdir(path.dirname(targetPath), { recursive: true }); + await copyFile(sourcePath, targetPath); + context.added.push(relative); + } + + const localConfig = { ...originalConfig }; + localConfig.xCommandEnabled = true; + await writeText(localConfigPath, `${JSON.stringify(localConfig, null, 2)}\n`); + + await validateInstalledFiles(targetRoot, manifest); + if (!options.skipTests) runServerVerifiers(targetRoot); + + const state = { + schemaVersion: 1, + overlayVersion: manifest.overlayVersion, + targetRelease: manifest.targetRelease, + installedAt: new Date().toISOString(), + backupDirectory: path.relative(targetRoot, backupRoot).replaceAll(path.sep, "/"), + originalConfigExists, + originalConfigHadXCommandEnabled, + originalXCommandEnabled: originalConfigHadXCommandEnabled + ? originalConfig.xCommandEnabled + : null, + }; + await mkdir(stateRoot, { recursive: true }); + await writeText(statePath, `${JSON.stringify(state, null, 2)}\n`); + console.log(`X Command server bridge ${manifest.overlayVersion} installed successfully.`); + console.log("Native and Docker startup now expose the same-origin bridge at http://127.0.0.1:26500/."); + } catch (error) { + for (const relative of context.added.reverse()) { + await rm(path.join(targetRoot, relative), { force: true }); + } + for (const relative of context.shared.reverse()) { + await copyFile(path.join(backupRoot, relative), path.join(targetRoot, relative)); + } + if (context.originalConfigExists) { + await writeText(localConfigPath, context.originalConfigText); + } else { + await rm(localConfigPath, { force: true }); + } + throw error; + } +} + +async function verify(targetRoot, manifest, options) { + await validateInstalledFiles(targetRoot, manifest); + if (!options.skipTests) runServerVerifiers(targetRoot); + console.log(`X Command server bridge ${manifest.overlayVersion} verification passed.`); +} + +async function uninstall(targetRoot, manifest, options) { + await requireStoppedServer(); + const stateRoot = path.join(targetRoot, "_local", "x-command"); + const statePath = path.join(stateRoot, "install-state.json"); + if (!await exists(statePath)) { + console.log("X Command server bridge is not installed; nothing to remove."); + return; + } + const state = JSON.parse(await readFile(statePath, "utf8")); + const backupRoot = path.join(targetRoot, relativePath(state.backupDirectory)); + + if (!options.force) await validateInstalledFiles(targetRoot, manifest); + const localConfigPath = path.join(targetRoot, "evejs.config.x-eve.local.json"); + let localConfig = {}; + if (await exists(localConfigPath)) { + localConfig = JSON.parse(await readFile(localConfigPath, "utf8")); + if (!localConfig || Array.isArray(localConfig) || typeof localConfig !== "object") { + throw new Error("evejs.config.x-eve.local.json must contain a JSON object."); + } + } + if (state.originalConfigHadXCommandEnabled) { + localConfig.xCommandEnabled = state.originalXCommandEnabled; + } else { + delete localConfig.xCommandEnabled; + } + + for (const entry of manifest.sharedFiles) { + const relative = relativePath(entry.path); + const backupPath = path.join(backupRoot, relative); + if (!await exists(backupPath)) throw new Error(`Backup is missing: ${entry.path}`); + await copyFile(backupPath, path.join(targetRoot, relative)); + } + for (const entry of manifest.addedFiles) { + await rm(path.join(targetRoot, relativePath(entry.path)), { force: true }); + } + + if (!state.originalConfigExists && Object.keys(localConfig).length === 0) { + await rm(localConfigPath, { force: true }); + } else { + await writeText(localConfigPath, `${JSON.stringify(localConfig, null, 2)}\n`); + } + const receipt = { + ...state, + uninstalledAt: new Date().toISOString(), + }; + await writeText( + path.join(stateRoot, "last-uninstall.json"), + `${JSON.stringify(receipt, null, 2)}\n`, + ); + await rm(statePath, { force: true }); + console.log("X Command server bridge removed. The original v0.2.2 files were restored."); +} + +async function checkPackage(manifest) { + const failures = []; + const listedPaths = new Set(); + for (const entry of [...manifest.sharedFiles, ...manifest.addedFiles]) { + const normalized = String(entry.path || "").replaceAll("\\", "/"); + if ( + !normalized || + path.posix.isAbsolute(normalized) || + normalized.split("/").includes("..") + ) { + failures.push(`unsafe manifest path: ${entry.path}`); + } + if (listedPaths.has(normalized)) failures.push(`duplicate manifest path: ${entry.path}`); + listedPaths.add(normalized); + } + const sharedPaths = new Set(Object.keys(transforms)); + for (const entry of manifest.sharedFiles) { + if (!transforms[entry.path]) failures.push(`missing transformer: ${entry.path}`); + sharedPaths.delete(entry.path); + if (!/^[A-F0-9]{64}$/.test(entry.baselineSha256 || "")) { + failures.push(`invalid baseline hash: ${entry.path}`); + } + if (!/^[A-F0-9]{64}$/.test(entry.installedSha256 || "")) { + failures.push(`invalid installed hash: ${entry.path}`); + } + } + for (const extra of sharedPaths) failures.push(`unlisted transformer: ${extra}`); + for (const entry of manifest.addedFiles) { + const sourcePath = path.join(bridgeRoot, "files", relativePath(entry.path)); + if (!await exists(sourcePath)) { + failures.push(`missing packaged file: ${entry.path}`); + continue; + } + const actual = await sha256(sourcePath); + if (actual !== entry.sha256) failures.push(`hash mismatch: ${entry.path}`); + } + if (failures.length > 0) { + throw new Error(`Bridge package check failed:\n- ${failures.join("\n- ")}`); + } + console.log(`X Command bridge package ${manifest.overlayVersion} integrity check passed.`); +} + +function printHelp() { + console.log("X Command server bridge for X-Eve Living Universe v0.2.2 / EveJS v0.12.3.1"); + console.log(""); + console.log("Usage:"); + console.log(" node tools/x-command-bridge.mjs install --target C:\\path\\to\\LivingUniverse"); + console.log(" node tools/x-command-bridge.mjs verify --target C:\\path\\to\\LivingUniverse"); + console.log(" node tools/x-command-bridge.mjs uninstall --target C:\\path\\to\\LivingUniverse"); + console.log(" node tools/x-command-bridge.mjs check-package"); +} + +async function main() { + const options = parseArguments(process.argv.slice(2)); + const manifest = await loadManifest(); + if (options.command === "help" || options.command === "--help") { + printHelp(); + return; + } + if (options.command === "check-package") { + await checkPackage(manifest); + return; + } + const targetRoot = resolveTarget(options.target); + if (options.command === "install") await install(targetRoot, manifest, options); + else if (options.command === "verify") await verify(targetRoot, manifest, options); + else if (options.command === "uninstall") await uninstall(targetRoot, manifest, options); + else throw new Error(`Unknown command: ${options.command}`); +} + +main().catch((error) => { + console.error(`ERROR: ${error.message}`); + process.exitCode = 1; +});