fix(FN-1339): move executor status bar above mobile nav bar

This commit is contained in:
gsxdsm
2026-04-09 15:49:42 -07:00
parent 0e295c3239
commit 8f50526158
16 changed files with 267 additions and 49 deletions

View File

@@ -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();
});
});

View File

@@ -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");
}

View File

@@ -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();

View File

@@ -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 () => {

View File

@@ -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();

View File

@@ -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";

View File

@@ -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;
});
}

View File

@@ -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);

View File

@@ -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),

View File

@@ -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 };

View File

@@ -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>;