fix(FN-1339): move executor status bar above mobile nav bar
This commit is contained in:
@@ -199,13 +199,14 @@ describe("runDesktop", () => {
|
||||
expect(mocks.store.updateSettings).toHaveBeenCalledWith({ enginePaused: true });
|
||||
expect(mocks.app.listen).toHaveBeenCalledWith(0);
|
||||
|
||||
// In production mode (not dev), renderer uses embedded assets, so no FUSION_DASHBOARD_URL
|
||||
expect(mocks.spawn).toHaveBeenCalledWith(
|
||||
"electron-bin",
|
||||
["--enable-source-maps", "/repo/packages/desktop/dist/main.js"],
|
||||
expect.objectContaining({
|
||||
cwd: "/repo",
|
||||
env: expect.objectContaining({
|
||||
FUSION_DASHBOARD_URL: "http://localhost:4545",
|
||||
// No FUSION_DASHBOARD_URL in production
|
||||
FUSION_SERVER_PORT: "4545",
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -113,23 +113,28 @@ export async function runDesktop(options: RunDesktopOptions = {}): Promise<void>
|
||||
}
|
||||
|
||||
const runtime = await startDashboardRuntime(rootDir, Boolean(options.paused));
|
||||
const rendererUrl = options.dev
|
||||
? process.env.FUSION_DASHBOARD_URL ?? "http://localhost:5173"
|
||||
: `http://localhost:${runtime.port}`;
|
||||
|
||||
const electronBinary = resolveElectronBinary();
|
||||
const desktopEntry = join(rootDir, "packages", "desktop", "dist", "main.js");
|
||||
const electronArgs = ["--enable-source-maps", desktopEntry, ...(options.dev ? ["--dev"] : [])];
|
||||
|
||||
// Build environment for Electron process
|
||||
const electronEnv: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
FUSION_SERVER_PORT: String(runtime.port),
|
||||
};
|
||||
|
||||
// In dev mode, set FUSION_DASHBOARD_URL to the dashboard runtime URL
|
||||
// In production mode, renderer uses embedded assets (no FUSION_DASHBOARD_URL needed)
|
||||
if (options.dev) {
|
||||
electronEnv.FUSION_DASHBOARD_URL = process.env.FUSION_DASHBOARD_URL ?? "http://localhost:5173";
|
||||
electronEnv.NODE_ENV = "development";
|
||||
}
|
||||
|
||||
const electronProcess = spawn(electronBinary, electronArgs, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
FUSION_DASHBOARD_URL: rendererUrl,
|
||||
FUSION_SERVER_PORT: String(runtime.port),
|
||||
...(options.dev ? { NODE_ENV: "development" } : {}),
|
||||
},
|
||||
env: electronEnv,
|
||||
});
|
||||
|
||||
let isShuttingDown = false;
|
||||
|
||||
@@ -20956,6 +20956,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
|
||||
font-size: 11px;
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
bottom: calc(56px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.executor-status-bar__segment {
|
||||
@@ -27304,7 +27305,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
|
||||
}
|
||||
|
||||
.mobile-nav-bar--with-footer {
|
||||
bottom: 32px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* Content padding: mobile nav only (no footer) */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Electron desktop shell for Fusion.
|
||||
|
||||
This package provides a native Electron wrapper around the existing Fusion dashboard web UI. The desktop shell connects to a running dashboard server and presents native desktop affordances including a system tray and application menu.
|
||||
This package provides a native Electron wrapper around the existing Fusion dashboard web UI. The desktop shell presents native desktop affordances including a system tray and application menu, with an embedded renderer for production deployments.
|
||||
|
||||
## Running the Desktop Shell
|
||||
|
||||
@@ -29,13 +29,36 @@ By default it uses `http://localhost:5173`. Override with `FUSION_DASHBOARD_URL`
|
||||
fn desktop
|
||||
```
|
||||
|
||||
`fn desktop` builds desktop artifacts, starts an embedded dashboard server on an ephemeral port, and launches Electron against that server.
|
||||
`fn desktop` builds desktop artifacts, starts an embedded dashboard server on an ephemeral port, and launches Electron with embedded renderer assets.
|
||||
|
||||
Useful flags:
|
||||
|
||||
- `fn desktop --dev` — use dev renderer URL (`FUSION_DASHBOARD_URL` or `http://localhost:5173`)
|
||||
- `fn desktop --paused` — start with engine paused
|
||||
|
||||
## Renderer Architecture
|
||||
|
||||
The desktop uses a dual-mode renderer strategy:
|
||||
|
||||
### Production Mode (default)
|
||||
- Loads embedded dashboard assets from `dist/client/` (bundled at build time)
|
||||
- Uses `window.loadFile()` to load `dist/client/index.html`
|
||||
- Renderer connects to the embedded API server via IPC (`getServerPort()`)
|
||||
|
||||
### Development Mode (`--dev` or `NODE_ENV=development`)
|
||||
- Loads renderer from `FUSION_DASHBOARD_URL` (defaults to `http://localhost:5173`)
|
||||
- Uses `window.loadURL()` for live reload support
|
||||
- Renderer connects to the dev API server
|
||||
|
||||
### Renderer Resolution (`src/renderer.ts`)
|
||||
|
||||
```typescript
|
||||
isDevelopmentMode() // Checks NODE_ENV or --dev flag
|
||||
isUrlRenderer() // true in dev mode, false in production
|
||||
getRendererUrl() // Returns URL or file:// path
|
||||
getRendererFilePath() // Returns absolute file path for loadFile()
|
||||
```
|
||||
|
||||
## IPC Channel Reference
|
||||
|
||||
`src/ipc.ts` registers the renderer ↔ main process bridge used by `window.fusionAPI`.
|
||||
@@ -50,6 +73,7 @@ Useful flags:
|
||||
| `window:isMaximized` | renderer → main | none | `Promise<boolean>` |
|
||||
| `app:getSystemInfo` | renderer → main | none | `Promise<{ platform; arch; electronVersion; nodeVersion; appVersion; }>` |
|
||||
| `app:checkForUpdates` | renderer → main | none | `Promise<{ status: "checking" } \| { status: "error"; error: string }>` |
|
||||
| `app:getServerPort` | renderer → main | none | `Promise<number \| undefined>` |
|
||||
| `tray:updateStatus` | renderer → main | `status: "running" \| "paused" \| "stopped"` | `Promise<void>` |
|
||||
| `native:showExportDialog` | renderer → main | none | `Promise<string \| null>` |
|
||||
| `native:showImportDialog` | renderer → main | none | `Promise<string \| null>` |
|
||||
@@ -96,7 +120,7 @@ Useful flags:
|
||||
`src/preload.ts` exposes a safe, context-isolated bridge:
|
||||
|
||||
- Window control: `minimize()`, `maximize()`, `close()`, `isMaximized()`
|
||||
- App/system: `getSystemInfo()`, `checkForUpdates()`
|
||||
- App/system: `getSystemInfo()`, `checkForUpdates()`, `getServerPort()`
|
||||
- Tray: `updateTrayStatus(status)`
|
||||
- Native dialogs: `showExportDialog()`, `showImportDialog()`
|
||||
- Event subscriptions (return unsubscribe functions):
|
||||
@@ -245,25 +269,36 @@ Run `pnpm --filter @fusion/desktop build` before `pack`/`dist` to ensure `dist/`
|
||||
|
||||
## Environment
|
||||
|
||||
- `FUSION_DASHBOARD_URL` — override the default dashboard URL used by the desktop shell (`http://localhost:4040`)
|
||||
- `FUSION_DASHBOARD_URL` — override the default dashboard URL in development mode (`http://localhost:5173`)
|
||||
- `FUSION_SERVER_PORT` — internal: port for embedded API server (set by CLI)
|
||||
- `FUSION_ELECTRON_BINARY` — path to Electron binary (for testing)
|
||||
|
||||
## Renderer Architecture
|
||||
## Build Pipeline
|
||||
|
||||
The desktop package now includes a renderer layer under `src/renderer/` that adapts the dashboard UI for Electron while preserving web-dashboard compatibility.
|
||||
### Development Build (`pnpm --filter @fusion/desktop dev`)
|
||||
1. Bundle `main.ts` and `preload.ts` with esbuild
|
||||
2. Start dashboard Vite dev server
|
||||
3. Launch Electron with `--dev` flag
|
||||
|
||||
### Electron-aware API transport
|
||||
### Production Build (`pnpm --filter @fusion/desktop build`)
|
||||
1. Build dashboard client to `packages/dashboard/dist/client/`
|
||||
2. Bundle `main.ts` and `preload.ts` with esbuild
|
||||
3. Copy dashboard client to `packages/desktop/dist/client/`
|
||||
|
||||
- `src/renderer/api-electron.ts` provides `createApiClient()` with runtime detection.
|
||||
- In browser/web contexts, it uses a standard fetch transport.
|
||||
- In Electron contexts, it uses an IPC transport (`electronAPI.invoke("api-request", ...)`) and can resolve the dashboard server port dynamically via `electronAPI.getServerPort()`.
|
||||
### CLI Launch (`fn desktop`)
|
||||
1. Build desktop artifacts (unless `--dev`)
|
||||
2. Start embedded API server on ephemeral port
|
||||
3. Launch Electron:
|
||||
- **Production:** Uses embedded renderer assets, `getServerPort()` for API connection
|
||||
- **Development (`--dev`):** Uses `FUSION_DASHBOARD_URL` for live reload
|
||||
|
||||
### Desktop shell UI components
|
||||
## Desktop Shell UI Components
|
||||
|
||||
- `src/renderer/components/DesktopWrapper.tsx` wraps the dashboard app for Electron-only chrome.
|
||||
- `src/renderer/components/TitleBar.tsx` implements a custom frameless title bar with Fusion branding, drag region behavior, and window controls (minimize/maximize/close).
|
||||
- The title bar styling lives in `src/renderer/components/TitleBar.css` and uses dashboard theme tokens (`--surface`, `--border`, `--text`, etc.).
|
||||
|
||||
### Desktop hooks
|
||||
## Desktop Hooks
|
||||
|
||||
Reusable renderer hooks in `src/renderer/hooks/` expose Electron runtime capabilities:
|
||||
|
||||
@@ -271,7 +306,7 @@ Reusable renderer hooks in `src/renderer/hooks/` expose Electron runtime capabil
|
||||
- `useAutoUpdate()` — update-available subscription + install trigger
|
||||
- `useDeepLink()` — deep-link subscription and `fusion://task/...` / `fusion://project/...` parsing
|
||||
|
||||
### Renderer entrypoint
|
||||
## Renderer Entrypoint
|
||||
|
||||
- `src/renderer/index.html` mirrors dashboard theme initialization logic with Electron-safe defaults.
|
||||
- `src/renderer/index.tsx` mounts the dashboard app in `StrictMode` and wraps it in `DesktopWrapper`.
|
||||
|
||||
@@ -92,6 +92,7 @@ describe("ipc handlers", () => {
|
||||
"window:isMaximized",
|
||||
"app:getSystemInfo",
|
||||
"app:checkForUpdates",
|
||||
"app:getServerPort",
|
||||
"tray:updateStatus",
|
||||
"native:showExportDialog",
|
||||
"native:showImportDialog",
|
||||
@@ -217,4 +218,25 @@ describe("ipc handlers", () => {
|
||||
|
||||
expect(mocks.updateTrayStatus).toHaveBeenCalledWith(tray, "paused");
|
||||
});
|
||||
|
||||
it("app:getServerPort returns port from environment", async () => {
|
||||
process.env.FUSION_SERVER_PORT = "4545";
|
||||
await registerHandlers();
|
||||
|
||||
const handler = mocks.ipcHandlers.get("app:getServerPort");
|
||||
const result = await handler?.({});
|
||||
|
||||
expect(result).toBe(4545);
|
||||
delete process.env.FUSION_SERVER_PORT;
|
||||
});
|
||||
|
||||
it("app:getServerPort returns undefined when env var not set", async () => {
|
||||
delete process.env.FUSION_SERVER_PORT;
|
||||
await registerHandlers();
|
||||
|
||||
const handler = mocks.ipcHandlers.get("app:getServerPort");
|
||||
const result = await handler?.({});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ const mocks = vi.hoisted(() => {
|
||||
|
||||
return {
|
||||
loadURL: vi.fn(() => Promise.resolve()),
|
||||
loadFile: vi.fn(() => Promise.resolve()),
|
||||
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
||||
listeners.set(event, handler);
|
||||
}),
|
||||
@@ -152,6 +153,16 @@ vi.mock("../native.js", () => ({
|
||||
DEFAULT_WINDOW_STATE: mocks.DEFAULT_WINDOW_STATE,
|
||||
}));
|
||||
|
||||
// Mock renderer module
|
||||
vi.mock("../renderer.js", () => ({
|
||||
isDevelopmentMode: vi.fn(() => false),
|
||||
getRendererUrl: vi.fn(() => "file:///path/to/dist/client/index.html"),
|
||||
getRendererFilePath: vi.fn(() => "/path/to/dist/client/index.html"),
|
||||
isUrlRenderer: vi.fn(() => false),
|
||||
IS_DEVELOPMENT: false,
|
||||
DASHBOARD_URL: "file:///path/to/dist/client/index.html",
|
||||
}));
|
||||
|
||||
async function importMainModule() {
|
||||
return import("../main.ts");
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => {
|
||||
|
||||
const browserWindow = {
|
||||
loadURL: vi.fn(),
|
||||
loadFile: vi.fn(),
|
||||
on: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
show: vi.fn(),
|
||||
@@ -75,6 +76,16 @@ vi.mock("../native.js", () => ({
|
||||
setupAutoUpdater: mocks.setupAutoUpdater,
|
||||
}));
|
||||
|
||||
// Mock renderer module
|
||||
vi.mock("../renderer.js", () => ({
|
||||
isDevelopmentMode: vi.fn(() => false),
|
||||
getRendererUrl: vi.fn(() => "file:///path/to/dist/client/index.html"),
|
||||
getRendererFilePath: vi.fn(() => "/path/to/dist/client/index.html"),
|
||||
isUrlRenderer: vi.fn(() => false),
|
||||
IS_DEVELOPMENT: false,
|
||||
DASHBOARD_URL: "file:///path/to/dist/client/index.html",
|
||||
}));
|
||||
|
||||
describe("main module integration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const mocks = vi.hoisted(() => {
|
||||
const browserWindowInstance = {
|
||||
loadURL: vi.fn(),
|
||||
loadFile: vi.fn(),
|
||||
on: vi.fn(),
|
||||
isVisible: vi.fn(() => true),
|
||||
show: vi.fn(),
|
||||
@@ -74,12 +75,23 @@ vi.mock("electron", () => ({
|
||||
shell: mocks.shell,
|
||||
}));
|
||||
|
||||
// Mock renderer module
|
||||
vi.mock("../renderer.js", () => ({
|
||||
isDevelopmentMode: vi.fn(() => false),
|
||||
getRendererUrl: vi.fn(() => "file:///path/to/dist/client/index.html"),
|
||||
getRendererFilePath: vi.fn(() => "/path/to/dist/client/index.html"),
|
||||
isUrlRenderer: vi.fn(() => false),
|
||||
IS_DEVELOPMENT: false,
|
||||
DASHBOARD_URL: "file:///path/to/dist/client/index.html",
|
||||
}));
|
||||
|
||||
async function importMainModule() {
|
||||
return import("../main.ts");
|
||||
}
|
||||
|
||||
describe("main process", () => {
|
||||
const originalDashboardUrl = process.env.FUSION_DASHBOARD_URL;
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -89,6 +101,16 @@ describe("main process", () => {
|
||||
} else {
|
||||
process.env.FUSION_DASHBOARD_URL = originalDashboardUrl;
|
||||
}
|
||||
if (originalNodeEnv === undefined) {
|
||||
delete process.env.NODE_ENV;
|
||||
} else {
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
}
|
||||
// Ensure we're in production mode for these tests
|
||||
vi.mocked(require("../renderer.js")).isDevelopmentMode.mockReturnValue(false);
|
||||
vi.mocked(require("../renderer.js")).getRendererUrl.mockReturnValue("file:///path/to/dist/client/index.html");
|
||||
vi.mocked(require("../renderer.js")).getRendererFilePath.mockReturnValue("/path/to/dist/client/index.html");
|
||||
vi.mocked(require("../renderer.js")).isUrlRenderer.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("DASHBOARD_URL defaults to local file URL in production mode", async () => {
|
||||
@@ -100,8 +122,13 @@ describe("main process", () => {
|
||||
expect(DASHBOARD_URL).toContain("/client/index.html");
|
||||
});
|
||||
|
||||
it("DASHBOARD_URL uses env override", async () => {
|
||||
it("DASHBOARD_URL uses env override in development mode", async () => {
|
||||
process.env.FUSION_DASHBOARD_URL = "http://localhost:5050";
|
||||
// Mock development mode to use the env var
|
||||
vi.mocked(require("../renderer.js")).isDevelopmentMode.mockReturnValue(true);
|
||||
vi.mocked(require("../renderer.js")).getRendererUrl.mockReturnValue("http://localhost:5050");
|
||||
vi.mocked(require("../renderer.js")).getRendererFilePath.mockReturnValue("");
|
||||
vi.mocked(require("../renderer.js")).isUrlRenderer.mockReturnValue(true);
|
||||
|
||||
const { DASHBOARD_URL } = await importMainModule();
|
||||
|
||||
@@ -129,12 +156,30 @@ describe("main process", () => {
|
||||
expect(options.webPreferences.preload).toContain("preload.js");
|
||||
});
|
||||
|
||||
it("createMainWindow loads the dashboard URL", async () => {
|
||||
const { createMainWindow, DASHBOARD_URL } = await importMainModule();
|
||||
it("createMainWindow loads the renderer URL in URL mode", async () => {
|
||||
vi.mocked(require("../renderer.js")).isUrlRenderer.mockReturnValue(true);
|
||||
vi.mocked(require("../renderer.js")).getRendererUrl.mockReturnValue("http://localhost:3000/index.html");
|
||||
vi.mocked(require("../renderer.js")).getRendererFilePath.mockReturnValue("");
|
||||
|
||||
const { createMainWindow } = await importMainModule();
|
||||
|
||||
createMainWindow();
|
||||
|
||||
expect(mocks.browserWindowInstance.loadURL).toHaveBeenCalledWith(DASHBOARD_URL);
|
||||
expect(mocks.browserWindowInstance.loadURL).toHaveBeenCalledWith("http://localhost:3000/index.html");
|
||||
expect(mocks.browserWindowInstance.loadFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("createMainWindow loads the renderer file in file mode (production)", async () => {
|
||||
vi.mocked(require("../renderer.js")).isUrlRenderer.mockReturnValue(false);
|
||||
vi.mocked(require("../renderer.js")).getRendererUrl.mockReturnValue("file:///path/to/dist/client/index.html");
|
||||
vi.mocked(require("../renderer.js")).getRendererFilePath.mockReturnValue("/path/to/dist/client/index.html");
|
||||
|
||||
const { createMainWindow } = await importMainModule();
|
||||
|
||||
createMainWindow();
|
||||
|
||||
expect(mocks.browserWindowInstance.loadFile).toHaveBeenCalledWith("/path/to/dist/client/index.html");
|
||||
expect(mocks.browserWindowInstance.loadURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports initializeApp for lifecycle orchestration", async () => {
|
||||
|
||||
@@ -33,6 +33,7 @@ function getFusionApi() {
|
||||
isMaximized: () => Promise<boolean>;
|
||||
getSystemInfo: () => Promise<unknown>;
|
||||
checkForUpdates: () => Promise<unknown>;
|
||||
getServerPort: () => Promise<number | undefined>;
|
||||
updateTrayStatus: (status: string) => Promise<void>;
|
||||
showExportDialog: () => Promise<string | null>;
|
||||
showImportDialog: () => Promise<string | null>;
|
||||
@@ -113,6 +114,15 @@ describe("preload", () => {
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("app:checkForUpdates");
|
||||
});
|
||||
|
||||
it("getServerPort invokes app:getServerPort", async () => {
|
||||
await importPreloadModule();
|
||||
|
||||
const api = getFusionApi();
|
||||
await api?.getServerPort();
|
||||
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("app:getServerPort");
|
||||
});
|
||||
|
||||
it("updateTrayStatus invokes tray:updateStatus with status argument", async () => {
|
||||
await importPreloadModule();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { DASHBOARD_URL, createMainWindow, initializeApp, run } from "./main.js";
|
||||
export { DASHBOARD_URL, IS_DEVELOPMENT, getRendererUrl, getRendererFilePath, isDevelopmentMode, isUrlRenderer } from "./renderer.js";
|
||||
export { createMainWindow, initializeApp, run } from "./main.js";
|
||||
export { registerIpcHandlers } from "./ipc.js";
|
||||
|
||||
export * from "./tray.js";
|
||||
|
||||
@@ -50,4 +50,10 @@ export function registerIpcHandlers(mainWindow: BrowserWindow, tray: Tray): void
|
||||
|
||||
ipcMain.handle("native:showExportDialog", () => showExportSettingsDialog(mainWindow));
|
||||
ipcMain.handle("native:showImportDialog", () => showImportSettingsDialog(mainWindow));
|
||||
|
||||
// Return the server port from environment variable (set by CLI)
|
||||
ipcMain.handle("app:getServerPort", () => {
|
||||
const port = process.env.FUSION_SERVER_PORT;
|
||||
return port ? parseInt(port, 10) : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { app, BrowserWindow, nativeImage, Tray } from "electron";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setupDeepLinkHandler, registerDeepLinkProtocol } from "./deep-link.js";
|
||||
import { registerIpcHandlers } from "./ipc.js";
|
||||
import { buildAppMenu } from "./menu.js";
|
||||
@@ -12,26 +12,16 @@ import {
|
||||
type WindowState,
|
||||
} from "./native.js";
|
||||
import { setupTray } from "./tray.js";
|
||||
import { getRendererUrl, getRendererFilePath, IS_DEVELOPMENT, isUrlRenderer } from "./renderer.js";
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export { IS_DEVELOPMENT } from "./renderer.js";
|
||||
export { DASHBOARD_URL } from "./renderer.js";
|
||||
|
||||
interface AppWithQuitFlag {
|
||||
isQuitting?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_DEV_DASHBOARD_URL = "http://localhost:5173";
|
||||
|
||||
function isDevelopmentMode(): boolean {
|
||||
return process.env.NODE_ENV === "development" || process.argv.includes("--dev");
|
||||
}
|
||||
|
||||
const PRODUCTION_DASHBOARD_URL = pathToFileURL(
|
||||
join(import.meta.dirname, "client", "index.html"),
|
||||
).toString();
|
||||
|
||||
export const IS_DEVELOPMENT = isDevelopmentMode();
|
||||
export const DASHBOARD_URL = process.env.FUSION_DASHBOARD_URL ?? (
|
||||
IS_DEVELOPMENT ? DEFAULT_DEV_DASHBOARD_URL : PRODUCTION_DASHBOARD_URL
|
||||
);
|
||||
|
||||
function enableSourceMaps(): void {
|
||||
const processWithSourceMaps = process as NodeJS.Process & {
|
||||
setSourceMapsEnabled?: (enabled: boolean) => void;
|
||||
@@ -63,7 +53,12 @@ export function createMainWindow(state?: WindowState): BrowserWindow {
|
||||
},
|
||||
});
|
||||
|
||||
void window.loadURL(DASHBOARD_URL);
|
||||
// Use renderer module to determine how to load the UI
|
||||
if (isUrlRenderer()) {
|
||||
void window.loadURL(getRendererUrl());
|
||||
} else {
|
||||
void window.loadFile(getRendererFilePath());
|
||||
}
|
||||
|
||||
window.on("close", (event) => {
|
||||
saveWindowState(window);
|
||||
|
||||
@@ -13,6 +13,7 @@ contextBridge.exposeInMainWorld("fusionAPI", {
|
||||
// App info
|
||||
getSystemInfo: (): Promise<SystemInfo> => ipcRenderer.invoke("app:getSystemInfo"),
|
||||
checkForUpdates: (): Promise<UpdateCheckResult> => ipcRenderer.invoke("app:checkForUpdates"),
|
||||
getServerPort: (): Promise<number | undefined> => ipcRenderer.invoke("app:getServerPort"),
|
||||
|
||||
// Tray status
|
||||
updateTrayStatus: (status: string): Promise<void> => ipcRenderer.invoke("tray:updateStatus", status),
|
||||
|
||||
@@ -1,3 +1,71 @@
|
||||
// This file is a placeholder. The Electron shell currently connects to the
|
||||
// Fusion dashboard server via URL. Future tasks will embed the dashboard's
|
||||
// built client assets here.
|
||||
/**
|
||||
* Renderer entry resolution for Electron desktop shell.
|
||||
*
|
||||
* Production: loads embedded renderer assets from dist/client/index.html
|
||||
* Development: loads from FUSION_DASHBOARD_URL or localhost:5173
|
||||
*/
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { join } from "node:path";
|
||||
|
||||
const DEFAULT_DEV_DASHBOARD_URL = "http://localhost:5173";
|
||||
|
||||
/**
|
||||
* Determines if the app is running in development mode.
|
||||
* Development mode is active when:
|
||||
* - NODE_ENV is "development", OR
|
||||
* - --dev flag is passed in command line arguments
|
||||
*/
|
||||
export function isDevelopmentMode(): boolean {
|
||||
return process.env.NODE_ENV === "development" || process.argv.includes("--dev");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the renderer URL for loading the dashboard UI.
|
||||
*
|
||||
* In development mode: uses FUSION_DASHBOARD_URL env var or defaults to localhost:5173
|
||||
* In production mode: loads from embedded renderer assets (file:// path to dist/client/index.html)
|
||||
*/
|
||||
export function getRendererUrl(): string {
|
||||
if (isDevelopmentMode()) {
|
||||
return process.env.FUSION_DASHBOARD_URL ?? DEFAULT_DEV_DASHBOARD_URL;
|
||||
}
|
||||
|
||||
// Production: use embedded renderer assets
|
||||
// This path is relative to the bundled main.js location
|
||||
const rendererIndexPath = join(import.meta.dirname, "client", "index.html");
|
||||
return pathToFileURL(rendererIndexPath).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the renderer file path for loadFile() calls.
|
||||
* Returns the absolute file path (not a URL).
|
||||
*/
|
||||
export function getRendererFilePath(): string {
|
||||
if (isDevelopmentMode()) {
|
||||
// In development, we use loadURL, not loadFile
|
||||
return "";
|
||||
}
|
||||
|
||||
// Production: return the absolute file path
|
||||
return join(import.meta.dirname, "client", "index.html");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the renderer should be loaded from a URL (development)
|
||||
* vs file path (production).
|
||||
*/
|
||||
export function isUrlRenderer(): boolean {
|
||||
if (isDevelopmentMode()) {
|
||||
// In dev, always use URL unless explicitly told to use file path
|
||||
const override = process.env.FUSION_USE_FILE_RENDERER;
|
||||
return override !== "true";
|
||||
}
|
||||
|
||||
// In production, always use file path
|
||||
return false;
|
||||
}
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export const IS_DEVELOPMENT = isDevelopmentMode();
|
||||
export { getRendererUrl as DASHBOARD_URL };
|
||||
|
||||
1
packages/desktop/src/types.d.ts
vendored
1
packages/desktop/src/types.d.ts
vendored
@@ -27,6 +27,7 @@ export interface FusionAPI {
|
||||
// App info
|
||||
getSystemInfo(): Promise<SystemInfo>;
|
||||
checkForUpdates(): Promise<UpdateCheckResult>;
|
||||
getServerPort(): Promise<number | undefined>;
|
||||
|
||||
// Tray status
|
||||
updateTrayStatus(status: string): Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user