mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
[v3.8.50] feat(electron): add Remote Server Mode to attach to an external OmniRoute instance (#8799)
Validated in local merge-train T7 (ungrouped batch 2)
This commit is contained in:
@@ -114,19 +114,23 @@ Built applications are placed in `dist-electron/`:
|
||||
4. Launch from Applications.
|
||||
|
||||
> ⚠️ **Note:** The app is not signed with an Apple Developer certificate yet. If macOS blocks the app, run:
|
||||
>
|
||||
> ```bash
|
||||
> xattr -cr /Applications/OmniRoute.app
|
||||
> ```
|
||||
>
|
||||
> Or right-click the app → Open → Open (to bypass Gatekeeper on first launch).
|
||||
|
||||
### Windows
|
||||
|
||||
**Installer (Recommended):**
|
||||
|
||||
1. Download `OmniRoute.Setup.*.exe` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
|
||||
2. Run the installer.
|
||||
3. Launch from Start Menu or Desktop shortcut.
|
||||
|
||||
**Portable (No Installation):**
|
||||
|
||||
1. Download `OmniRoute.exe` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
|
||||
2. Run directly from any folder.
|
||||
|
||||
@@ -147,20 +151,44 @@ Built applications are placed in `dist-electron/`:
|
||||
- **Server Readiness** — Waits for health check before showing window
|
||||
- **System Tray** — Minimize to tray with quick actions (open, port change, quit)
|
||||
- **Port Management** — Change port from tray menu (server restarts automatically)
|
||||
- **Remote Server Mode** — Point the shell at an already-running OmniRoute server (e.g. a Docker/OrbStack container, or another machine) instead of spawning a local one — see below
|
||||
- **Window Controls** — Custom minimize, maximize, close via IPC
|
||||
- **Content Security Policy** — Restrictive CSP via session headers
|
||||
- **Offline Support** — Bundled Next.js standalone server
|
||||
- **Single Instance** — Only one app instance can run at a time
|
||||
|
||||
## Remote Server Mode
|
||||
|
||||
By default the desktop shell spawns and manages its own bundled Next.js server. If you
|
||||
already run OmniRoute elsewhere — most commonly in a Docker/OrbStack container, so
|
||||
provider credentials and env-var handling stay isolated from the host — you can point the
|
||||
shell at that instance instead, so it's purely a native window + tray onto a server you
|
||||
already run.
|
||||
|
||||
**Via the tray menu:** _Remote Server → Connect to Remote Server…_, enter the server's
|
||||
URL (e.g. `http://localhost:20128`), and save. Leave the field blank and save to
|
||||
disconnect and go back to the local embedded server. The preference persists across
|
||||
restarts in `<data dir>/electron-preferences.json` (see `DATA_DIR` above for where that
|
||||
lives on your platform).
|
||||
|
||||
**Via environment variable:** set `OMNIROUTE_REMOTE_URL` before launching the app (e.g.
|
||||
`OMNIROUTE_REMOTE_URL=http://localhost:20128 npm run dev`, or export it in the
|
||||
environment that launches the packaged app). The env var always wins over the persisted
|
||||
preference and is session-scoped — it doesn't get written to the prefs file.
|
||||
|
||||
Only `http://` and `https://` URLs are accepted; anything else is rejected before the
|
||||
window loads.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------- | ------------ | --------------------------------- |
|
||||
| `OMNIROUTE_PORT` | `20128` | Server port |
|
||||
| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit (64–16384 MB) |
|
||||
| `NODE_ENV` | `production` | Set to `development` for dev mode |
|
||||
| Variable | Default | Description |
|
||||
| ---------------------- | ------------ | ----------------------------------------------------------------------------------------------------- |
|
||||
| `OMNIROUTE_PORT` | `20128` | Server port |
|
||||
| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit (64–16384 MB) |
|
||||
| `OMNIROUTE_REMOTE_URL` | _(unset)_ | Attach to this server instead of spawning a local one — see [Remote Server Mode](#remote-server-mode) |
|
||||
| `NODE_ENV` | `production` | Set to `development` for dev mode |
|
||||
|
||||
### Custom Icon
|
||||
|
||||
@@ -175,12 +203,12 @@ Place your icons in `assets/`:
|
||||
|
||||
### Invoke (Renderer → Main, async)
|
||||
|
||||
| Channel | Returns | Description |
|
||||
| ---------------- | ------------- | --------------------------------------------- |
|
||||
| `get-app-info` | `AppInfo` | App name, version, platform, isDev, port |
|
||||
| `open-external` | `void` | Open URL in default browser (http/https only) |
|
||||
| `get-data-dir` | `string` | Get userData directory path |
|
||||
| `restart-server` | `{ success }` | Stop + restart server (5s timeout + SIGKILL) |
|
||||
| Channel | Returns | Description |
|
||||
| ---------------- | ------------- | --------------------------------------------------------- |
|
||||
| `get-app-info` | `AppInfo` | App name, version, platform, isDev, port, remoteServerUrl |
|
||||
| `open-external` | `void` | Open URL in default browser (http/https only) |
|
||||
| `get-data-dir` | `string` | Get userData directory path |
|
||||
| `restart-server` | `{ success }` | Stop + restart server (5s timeout + SIGKILL) |
|
||||
|
||||
### Send (Renderer → Main, fire-and-forget)
|
||||
|
||||
|
||||
86
electron/assets/remoteServerPrompt.html
Normal file
86
electron/assets/remoteServerPrompt.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'self'"
|
||||
/>
|
||||
<title>Connect to Remote Server</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family:
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e5e5e5;
|
||||
user-select: none;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: #a3a3a3;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #3f3f3f;
|
||||
background: #262626;
|
||||
color: #e5e5e5;
|
||||
}
|
||||
.error {
|
||||
color: #f87171;
|
||||
font-size: 12px;
|
||||
min-height: 16px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
button {
|
||||
padding: 7px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #3f3f3f;
|
||||
background: #262626;
|
||||
color: #e5e5e5;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.primary {
|
||||
background: #ff586b;
|
||||
border-color: #ff586b;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Point this desktop app at an already-running OmniRoute server (e.g. a Docker/OrbStack
|
||||
container) instead of spawning a local one. Leave blank and Save to disconnect.
|
||||
</p>
|
||||
<input
|
||||
id="url-input"
|
||||
type="text"
|
||||
placeholder="http://localhost:20128"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="actions">
|
||||
<button id="cancel-btn">Cancel</button>
|
||||
<button id="save-btn" class="primary">Save</button>
|
||||
</div>
|
||||
|
||||
<script src="../remoteServerPromptRenderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
75
electron/lib/remoteServerPreferences.js
Normal file
75
electron/lib/remoteServerPreferences.js
Normal file
@@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* remoteServerPreferences.js — pure read/write helpers for the small JSON
|
||||
* preferences file that persists the operator-configured remote server URL
|
||||
* across app restarts (see resolveRemoteServerUrl.js for how it's consumed).
|
||||
*
|
||||
* Deliberately a plain flat JSON file rather than the app's SQLite database:
|
||||
* this preference must be readable before deciding whether to spawn (or even
|
||||
* reach) the local server, so it cannot depend on any server-owned storage.
|
||||
*
|
||||
* Extracted as pure, dependency-injectable helpers so they can be unit-tested
|
||||
* without importing the full Electron main process.
|
||||
*
|
||||
* @param {string} prefsPath - absolute path to electron-preferences.json
|
||||
* @param {(p: string) => boolean} [existsSync]
|
||||
* @param {(p: string, enc: string) => string} [readFileSync]
|
||||
* @returns {{remoteServerUrl: string|null}}
|
||||
*/
|
||||
function readPreferences(prefsPath, existsSync = fs.existsSync, readFileSync = fs.readFileSync) {
|
||||
if (!existsSync(prefsPath)) return { remoteServerUrl: null };
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(prefsPath, "utf8"));
|
||||
const remoteServerUrl =
|
||||
typeof parsed.remoteServerUrl === "string" && parsed.remoteServerUrl.trim()
|
||||
? parsed.remoteServerUrl.trim()
|
||||
: null;
|
||||
return { remoteServerUrl };
|
||||
} catch {
|
||||
return { remoteServerUrl: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the remote server URL preference. Pass `null` to clear it (reverts
|
||||
* to spawning the local embedded server on next restart).
|
||||
*
|
||||
* @param {string} prefsPath
|
||||
* @param {string|null} remoteServerUrl
|
||||
* @param {(p: string) => boolean} [existsSync]
|
||||
* @param {(p: string, enc: string) => string} [readFileSync]
|
||||
* @param {(p: string, data: string, enc: string) => void} [writeFileSync]
|
||||
* @param {(p: string, opts: object) => void} [mkdirSync]
|
||||
*/
|
||||
function writeRemoteServerUrl(
|
||||
prefsPath,
|
||||
remoteServerUrl,
|
||||
{
|
||||
existsSync = fs.existsSync,
|
||||
readFileSync = fs.readFileSync,
|
||||
writeFileSync = fs.writeFileSync,
|
||||
mkdirSync = fs.mkdirSync,
|
||||
} = {}
|
||||
) {
|
||||
try {
|
||||
const dir = path.dirname(prefsPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const current = readPreferences(prefsPath, existsSync, readFileSync);
|
||||
const next = { ...current, remoteServerUrl: remoteServerUrl || null };
|
||||
writeFileSync(prefsPath, JSON.stringify(next, null, 2) + "\n", "utf8");
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[remoteServerPreferences] Failed to write preferences to ${prefsPath}:`,
|
||||
err instanceof Error ? err.message : String(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { readPreferences, writeRemoteServerUrl };
|
||||
79
electron/lib/resolveRemoteServerUrl.js
Normal file
79
electron/lib/resolveRemoteServerUrl.js
Normal file
@@ -0,0 +1,79 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
/**
|
||||
* resolveRemoteServerUrl.js — pure helper for resolving an operator-configured
|
||||
* remote OmniRoute server URL, so the Electron shell can attach to an
|
||||
* already-running instance (e.g. a Docker/OrbStack container, or a server on
|
||||
* another machine on the LAN) instead of spawning its own bundled Next.js
|
||||
* server.
|
||||
*
|
||||
* Some environments make the bundled local server impractical — for example,
|
||||
* a host that injects provider API keys via a secrets manager in a way the
|
||||
* packaged app's env-file loading doesn't expect. Running the real server in
|
||||
* an isolated container and pointing the desktop shell at it sidesteps that
|
||||
* entirely.
|
||||
*
|
||||
* Precedence:
|
||||
* 1. OMNIROUTE_REMOTE_URL env var (explicit, session-scoped override)
|
||||
* 2. `remoteServerUrl` key in <dataDir>/electron-preferences.json (persisted
|
||||
* via the tray menu's "Connect to Remote Server…" prompt)
|
||||
* 3. null — caller falls back to spawning the local embedded server
|
||||
*
|
||||
* Extracted as a pure helper (env + fs injectable) so it can be unit-tested
|
||||
* without importing the full Electron main process (which requires the
|
||||
* Electron binary).
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {NodeJS.ProcessEnv} opts.env - injectable process.env (for tests)
|
||||
* @param {string} opts.prefsPath - absolute path to electron-preferences.json
|
||||
* @param {(p: string) => boolean} [opts.existsSync] - injectable fs.existsSync
|
||||
* @param {(p: string, enc: string) => string} [opts.readFileSync] - injectable fs.readFileSync
|
||||
* @returns {string|null} the validated http(s) remote URL (no trailing slash), or null if none configured
|
||||
*/
|
||||
function resolveRemoteServerUrl({
|
||||
env,
|
||||
prefsPath,
|
||||
existsSync = fs.existsSync,
|
||||
readFileSync = fs.readFileSync,
|
||||
}) {
|
||||
const candidate = readCandidate({ env, prefsPath, existsSync, readFileSync });
|
||||
if (!candidate) return null;
|
||||
return isValidHttpUrl(candidate) ? stripTrailingSlash(candidate) : null;
|
||||
}
|
||||
|
||||
function readCandidate({ env, prefsPath, existsSync, readFileSync }) {
|
||||
const fromEnv = (env.OMNIROUTE_REMOTE_URL || "").trim();
|
||||
if (fromEnv) return fromEnv;
|
||||
|
||||
if (!prefsPath || !existsSync(prefsPath)) return null;
|
||||
try {
|
||||
const prefs = JSON.parse(readFileSync(prefsPath, "utf8"));
|
||||
const fromPrefs = typeof prefs.remoteServerUrl === "string" ? prefs.remoteServerUrl.trim() : "";
|
||||
return fromPrefs || null;
|
||||
} catch {
|
||||
// Corrupt/partial prefs file — fall back to spawning the local server
|
||||
// rather than crashing the app on startup.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} candidate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidHttpUrl(candidate) {
|
||||
try {
|
||||
const parsed = new URL(candidate);
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function stripTrailingSlash(url) {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
module.exports = { resolveRemoteServerUrl, isValidHttpUrl };
|
||||
139
electron/main.js
139
electron/main.js
@@ -37,6 +37,8 @@ const { loginManager } = require("./loginManager");
|
||||
const { killProcessTree } = require("./processTree");
|
||||
const { resolveServerEntry } = require("./lib/resolveServerEntry");
|
||||
const { resolveDarwinHelperExecutable } = require("./lib/resolveNodeHelper");
|
||||
const { resolveRemoteServerUrl, isValidHttpUrl } = require("./lib/resolveRemoteServerUrl");
|
||||
const { writeRemoteServerUrl } = require("./lib/remoteServerPreferences");
|
||||
|
||||
// ── Single Instance Lock ───────────────────────────────────
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@@ -67,8 +69,23 @@ let tray = null;
|
||||
let nextServer = null;
|
||||
let serverPort = 20128;
|
||||
let isServerStopped = false;
|
||||
let remoteServerPromptWindow = null;
|
||||
|
||||
const getServerUrl = () => `http://localhost:${serverPort}`;
|
||||
// ── Remote Server Mode ──────────────────────────────────────
|
||||
// Lets the desktop shell attach to an already-running OmniRoute server (e.g. a
|
||||
// Docker/OrbStack container, or another machine) instead of spawning its own
|
||||
// bundled Next.js server. See lib/resolveRemoteServerUrl.js for precedence
|
||||
// (OMNIROUTE_REMOTE_URL env var, then the persisted prefs file below).
|
||||
const REMOTE_SERVER_PREFS_PATH = path.join(
|
||||
resolveDataDir(null, process.env),
|
||||
"electron-preferences.json"
|
||||
);
|
||||
let remoteServerUrl = resolveRemoteServerUrl({
|
||||
env: process.env,
|
||||
prefsPath: REMOTE_SERVER_PREFS_PATH,
|
||||
});
|
||||
|
||||
const getServerUrl = () => remoteServerUrl || `http://localhost:${serverPort}`;
|
||||
|
||||
function resolveNodeExecutable(env = process.env) {
|
||||
// #1081: Ensure Next.js standalone runs using Electron's Node runtime
|
||||
@@ -456,6 +473,23 @@ function createTray() {
|
||||
{ label: "3000", click: () => changePort(3000) },
|
||||
{ label: "8080", click: () => changePort(8080) },
|
||||
],
|
||||
enabled: !remoteServerUrl,
|
||||
},
|
||||
{
|
||||
label: "Remote Server",
|
||||
submenu: [
|
||||
{
|
||||
label: remoteServerUrl ? `Connected: ${remoteServerUrl}` : "Using local embedded server",
|
||||
enabled: false,
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ label: "Connect to Remote Server…", click: () => showRemoteServerPrompt() },
|
||||
{
|
||||
label: "Disconnect (use Local Server)",
|
||||
enabled: Boolean(remoteServerUrl),
|
||||
click: () => setRemoteServerUrl(null),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
@@ -512,8 +546,97 @@ async function changePort(newPort) {
|
||||
console.log(`[Electron] Port changed: ${oldPort} → ${serverPort}`);
|
||||
}
|
||||
|
||||
// ── Remote Server Mode: prompt window ──────────────────────
|
||||
function showRemoteServerPrompt() {
|
||||
if (remoteServerPromptWindow && !remoteServerPromptWindow.isDestroyed()) {
|
||||
remoteServerPromptWindow.show();
|
||||
remoteServerPromptWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
remoteServerPromptWindow = new BrowserWindow({
|
||||
width: 480,
|
||||
height: 210,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false,
|
||||
title: "Connect to Remote Server",
|
||||
parent: mainWindow || undefined,
|
||||
modal: Boolean(mainWindow),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "remoteServerPromptPreload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
remoteServerPromptWindow.setMenuBarVisibility(false);
|
||||
remoteServerPromptWindow.loadFile(path.join(__dirname, "assets", "remoteServerPrompt.html"));
|
||||
|
||||
remoteServerPromptWindow.on("closed", () => {
|
||||
remoteServerPromptWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Remote Server Mode: apply a new URL (or clear it) ──────
|
||||
async function setRemoteServerUrl(nextUrl) {
|
||||
const normalized = (nextUrl || "").trim() || null;
|
||||
if (normalized === remoteServerUrl) return;
|
||||
|
||||
// Reject invalid URLs — only http:// and https:// are accepted.
|
||||
if (normalized !== null && !isValidHttpUrl(normalized)) {
|
||||
console.warn("[Electron] Rejected invalid remote server URL:", normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
sendToRenderer("server-status", { status: "restarting", port: serverPort });
|
||||
|
||||
// Stop any locally-spawned server before switching modes in either direction.
|
||||
const serverToStop = nextServer;
|
||||
stopNextServer();
|
||||
await waitForServerExit(serverToStop);
|
||||
|
||||
remoteServerUrl = normalized;
|
||||
writeRemoteServerUrl(REMOTE_SERVER_PREFS_PATH, remoteServerUrl);
|
||||
|
||||
startNextServer();
|
||||
try {
|
||||
await waitForServer(`${getServerUrl()}/api/monitoring/health`);
|
||||
} catch (err) {
|
||||
console.warn("[Electron] Server did not become ready after remote-server change:", err.message);
|
||||
}
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.loadURL(getServerUrl());
|
||||
}
|
||||
createTray();
|
||||
|
||||
sendToRenderer("server-status", {
|
||||
status: "running",
|
||||
port: serverPort,
|
||||
remoteUrl: remoteServerUrl,
|
||||
});
|
||||
console.log(
|
||||
remoteServerUrl
|
||||
? `[Electron] Now connected to remote server: ${remoteServerUrl}`
|
||||
: "[Electron] Disconnected from remote server — spawning local server again"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Server Lifecycle (#1, #5, #10) ─────────────────────────
|
||||
function startNextServer() {
|
||||
if (remoteServerUrl) {
|
||||
console.log("[Electron] Remote server mode — connecting to", remoteServerUrl);
|
||||
sendToRenderer("server-status", {
|
||||
status: "running",
|
||||
port: serverPort,
|
||||
remoteUrl: remoteServerUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
console.log("[Electron] Dev mode — connect to existing Next.js server");
|
||||
sendToRenderer("server-status", { status: "running", port: serverPort });
|
||||
@@ -777,8 +900,22 @@ function setupIpcHandlers() {
|
||||
platform: process.platform,
|
||||
isDev,
|
||||
port: serverPort,
|
||||
remoteServerUrl,
|
||||
}));
|
||||
|
||||
// ── Remote Server Mode: prompt window IPC (main-process-only trust
|
||||
// boundary — this window never loads remote/untrusted content) ──
|
||||
ipcMain.handle("remote-server-prompt:get-initial-url", () => remoteServerUrl || "");
|
||||
|
||||
ipcMain.on("remote-server-prompt:submit", (_event, url) => {
|
||||
remoteServerPromptWindow?.close();
|
||||
void setRemoteServerUrl(url);
|
||||
});
|
||||
|
||||
ipcMain.on("remote-server-prompt:cancel", () => {
|
||||
remoteServerPromptWindow?.close();
|
||||
});
|
||||
|
||||
ipcMain.handle("open-external", (_event, url) => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
@@ -60,8 +60,13 @@
|
||||
"loginManager.js",
|
||||
"processTree.js",
|
||||
"sqlite-inspection.js",
|
||||
"remoteServerPromptPreload.js",
|
||||
"remoteServerPromptRenderer.js",
|
||||
"lib/resolveServerEntry.js",
|
||||
"lib/resolveNodeHelper.js",
|
||||
"lib/resolveRemoteServerUrl.js",
|
||||
"lib/remoteServerPreferences.js",
|
||||
"assets/remoteServerPrompt.html",
|
||||
"package.json",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
|
||||
@@ -106,8 +106,15 @@ const VALID_CHANNELS = {
|
||||
"login:start",
|
||||
"login:cancel",
|
||||
"login:status",
|
||||
"remote-server-prompt:get-initial-url",
|
||||
],
|
||||
send: [
|
||||
"window-minimize",
|
||||
"window-maximize",
|
||||
"window-close",
|
||||
"remote-server-prompt:submit",
|
||||
"remote-server-prompt:cancel",
|
||||
],
|
||||
send: ["window-minimize", "window-maximize", "window-close"],
|
||||
receive: ["server-status", "port-changed", "update-status", "login:status"],
|
||||
};
|
||||
|
||||
@@ -160,6 +167,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
|
||||
// ── Receive (event listeners) ────────────────────────────
|
||||
// Fix #6: Returns a disposer function for precise cleanup
|
||||
// "server-status" payloads include remoteUrl when running in Remote Server
|
||||
// Mode (see electron/main.js setRemoteServerUrl) — surfaced here read-only;
|
||||
// the actual URL is configured via the tray menu, not the renderer.
|
||||
onServerStatus: (callback) => safeOn("server-status", callback),
|
||||
onPortChanged: (callback) => safeOn("port-changed", callback),
|
||||
onUpdateStatus: (callback) => safeOn("update-status", callback),
|
||||
|
||||
15
electron/remoteServerPromptPreload.js
Normal file
15
electron/remoteServerPromptPreload.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Preload for the small "Connect to Remote Server" prompt window.
|
||||
*
|
||||
* Kept separate from the main preload.js — this window only ever loads our
|
||||
* own bundled remoteServerPrompt.html (never remote/untrusted content), but we
|
||||
* still keep contextIsolation on and expose the minimum surface needed.
|
||||
*/
|
||||
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("remoteServerPrompt", {
|
||||
getInitialUrl: () => ipcRenderer.invoke("remote-server-prompt:get-initial-url"),
|
||||
submit: (url) => ipcRenderer.send("remote-server-prompt:submit", url),
|
||||
cancel: () => ipcRenderer.send("remote-server-prompt:cancel"),
|
||||
});
|
||||
40
electron/remoteServerPromptRenderer.js
Normal file
40
electron/remoteServerPromptRenderer.js
Normal file
@@ -0,0 +1,40 @@
|
||||
(function () {
|
||||
const input = document.getElementById("url-input");
|
||||
const errorEl = document.getElementById("error");
|
||||
const saveBtn = document.getElementById("save-btn");
|
||||
const cancelBtn = document.getElementById("cancel-btn");
|
||||
|
||||
function isValidOrEmpty(value) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return true; // empty = disconnect, handled by main process
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
window.remoteServerPrompt.getInitialUrl().then((url) => {
|
||||
input.value = url || "";
|
||||
input.focus();
|
||||
});
|
||||
|
||||
saveBtn.addEventListener("click", () => {
|
||||
const value = input.value.trim();
|
||||
if (!isValidOrEmpty(value)) {
|
||||
errorEl.textContent = "Enter a valid http:// or https:// URL, or leave blank to disconnect.";
|
||||
return;
|
||||
}
|
||||
window.remoteServerPrompt.submit(value);
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener("click", () => {
|
||||
window.remoteServerPrompt.cancel();
|
||||
});
|
||||
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") saveBtn.click();
|
||||
if (event.key === "Escape") cancelBtn.click();
|
||||
});
|
||||
})();
|
||||
4
electron/types.d.ts
vendored
4
electron/types.d.ts
vendored
@@ -14,11 +14,15 @@ export interface AppInfo {
|
||||
platform: "win32" | "darwin" | "linux";
|
||||
isDev: boolean;
|
||||
port: number;
|
||||
/** Set when Remote Server Mode is active (tray → Remote Server → Connect…). */
|
||||
remoteServerUrl: string | null;
|
||||
}
|
||||
|
||||
export interface ServerStatus {
|
||||
status: "starting" | "running" | "stopped" | "restarting" | "error";
|
||||
port: number;
|
||||
/** Present only while connected to a remote server instead of the embedded one. */
|
||||
remoteUrl?: string;
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
|
||||
Reference in New Issue
Block a user