feat(electron): add main process, preload, store, protocol, and type definitions
- Add ElectronAPI type definitions and runtime contract (isElectron guard) - Add electron-store with backend connection data model - Add centralized API base URL utility (api-base.ts) - Implement app:// custom protocol with API proxy - Implement preload script with full contextBridge API - Complete main process: BrowserWindow config, IPC handlers, window controls - Register app:// scheme as privileged for secure renderer access
This commit is contained in:
119
dashboard/src/lib/api-base.ts
Normal file
119
dashboard/src/lib/api-base.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Centralized API base URL utility
|
||||
* Provides single source of truth for all URL construction across the application
|
||||
* Handles environment-specific configuration (Electron, Browser DEV, Browser PROD)
|
||||
*/
|
||||
|
||||
import type { BackendConnection } from '@/types/electron'
|
||||
|
||||
import { isElectron } from './runtime'
|
||||
|
||||
/**
|
||||
* Get API base URL for HTTP/HTTPS requests
|
||||
* - Electron: User-configured backend URL from main process
|
||||
* - Browser DEV: Empty string (Vite proxy handles /api prefix)
|
||||
* - Browser PROD: Empty string (same-origin deployment)
|
||||
*/
|
||||
export async function getApiBaseUrl(): Promise<string> {
|
||||
if (isElectron()) {
|
||||
// Electron: Get configured backend URL from IPC
|
||||
const backendUrl = await window.electronAPI?.getActiveBackendUrl()
|
||||
return backendUrl ?? ''
|
||||
}
|
||||
|
||||
// Browser (DEV & PROD): Return empty string
|
||||
// In DEV: Vite proxy forwards /api requests to backend
|
||||
// In PROD: API is deployed on same origin as frontend
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebSocket base URL
|
||||
* - Electron: Convert HTTP/HTTPS URL to WS/WSS
|
||||
* - Browser DEV: ws://127.0.0.1:8001 (hardcoded, same as log-websocket.ts)
|
||||
* - Browser PROD: Construct WS URL from window.location
|
||||
*/
|
||||
export async function getWsBaseUrl(): Promise<string> {
|
||||
if (isElectron()) {
|
||||
// Electron: Convert API URL protocol to WS protocol
|
||||
const apiUrl = await getApiBaseUrl()
|
||||
if (!apiUrl) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Convert http -> ws, https -> wss
|
||||
return apiUrl.replace(/^https?/, (match) => {
|
||||
return match === 'https' ? 'wss' : 'ws'
|
||||
})
|
||||
}
|
||||
|
||||
// Browser DEV: Use hardcoded WebSocket server
|
||||
if (import.meta.env.DEV) {
|
||||
return 'ws://127.0.0.1:8001'
|
||||
}
|
||||
|
||||
// Browser PROD: Construct WS URL from current location
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
return `${protocol}//${host}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get synchronous API base URL for axios baseURL configuration
|
||||
* Note: axios instance baseURL is set at module initialization time (synchronous).
|
||||
* Since window.electronAPI.getActiveBackendUrl() is async, this function returns
|
||||
* empty string. The actual Electron backend URL will be injected via axios request
|
||||
* interceptor (Task 7) to support dynamic backend switching at runtime.
|
||||
*/
|
||||
export function getAxiosBaseUrl(): string {
|
||||
// Always return empty string:
|
||||
// - Browser: Vite proxy / same-origin handles paths
|
||||
// - Electron: axios interceptor injects dynamic baseURL
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve full API path by prepending base URL if needed
|
||||
* - Electron: Prepends configured backend URL
|
||||
* - Browser: Path remains unchanged (proxy/same-origin handling)
|
||||
*/
|
||||
export async function resolveApiPath(path: string): Promise<string> {
|
||||
if (isElectron()) {
|
||||
const baseUrl = await getApiBaseUrl()
|
||||
return baseUrl ? `${baseUrl}${path}` : path
|
||||
}
|
||||
|
||||
// Browser: Path is used as-is
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to backend URL changes
|
||||
* Electron: Listens to IPC backend change events
|
||||
* Browser: No-op (backend cannot change at runtime)
|
||||
*
|
||||
* @param callback Function called when backend URL changes
|
||||
* @returns Unsubscribe function
|
||||
*/
|
||||
export function onBackendUrlChanged(
|
||||
callback: (newUrl: string | null) => void
|
||||
): () => void {
|
||||
if (!isElectron()) {
|
||||
// Browser: No-op, return empty unsubscribe function
|
||||
return () => {}
|
||||
}
|
||||
|
||||
// Electron: Register IPC listener and return unsubscribe function
|
||||
if (!window.electronAPI?.onBackendChanged) {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
// Wrap callback to extract URL from BackendConnection
|
||||
const wrappedCallback = (backend: BackendConnection | null) => {
|
||||
const url = backend?.url ?? null
|
||||
callback(url)
|
||||
}
|
||||
|
||||
// Get and return the unsubscribe function from preload
|
||||
return window.electronAPI.onBackendChanged(wrappedCallback)
|
||||
}
|
||||
77
dashboard/src/lib/runtime.ts
Normal file
77
dashboard/src/lib/runtime.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Runtime environment detection and information
|
||||
* Provides unified interface for checking execution environment (Electron vs Browser)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type of runtime environment
|
||||
*/
|
||||
export type RuntimeKind = 'electron' | 'browser'
|
||||
|
||||
/**
|
||||
* Runtime information object
|
||||
*/
|
||||
export interface RuntimeInfo {
|
||||
/** Type of runtime (electron or browser) */
|
||||
kind: RuntimeKind
|
||||
/** Version information (electron versions, etc) */
|
||||
versions?: Record<string, string>
|
||||
/** User agent string */
|
||||
userAgent?: string
|
||||
/** Source of runtime detection (tag means set by preload, fallback means default for browser) */
|
||||
source: 'tag' | 'fallback'
|
||||
}
|
||||
|
||||
/**
|
||||
* Build default browser runtime info
|
||||
* Used as fallback when not in Electron environment
|
||||
*/
|
||||
function buildBrowserRuntime(): RuntimeInfo {
|
||||
return {
|
||||
kind: 'browser',
|
||||
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
|
||||
source: 'fallback',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current runtime information
|
||||
* Reads from globalThis.__RUNTIME__ if available (set by Electron preload)
|
||||
* Falls back to browser runtime if not running in Electron
|
||||
*/
|
||||
export function getRuntime(): RuntimeInfo {
|
||||
// Check if running in Electron (preload sets __RUNTIME__)
|
||||
if (typeof globalThis !== 'undefined' && globalThis.__RUNTIME__) {
|
||||
return globalThis.__RUNTIME__
|
||||
}
|
||||
|
||||
// Fallback to browser runtime
|
||||
return buildBrowserRuntime()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running in Electron environment
|
||||
* Safe to use across browser and Electron - always returns boolean
|
||||
*/
|
||||
export function isElectron(): boolean {
|
||||
return getRuntime().kind === 'electron'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform information
|
||||
* In Electron: calls window.electronAPI.getPlatform() for actual platform
|
||||
* In browser: returns 'browser' as identifier
|
||||
*/
|
||||
export function getPlatform(): string {
|
||||
if (!isElectron()) {
|
||||
return 'browser'
|
||||
}
|
||||
|
||||
// Safe to access electronAPI because isElectron() confirms it's available
|
||||
if (typeof window !== 'undefined' && window.electronAPI?.getPlatform) {
|
||||
return window.electronAPI.getPlatform()
|
||||
}
|
||||
|
||||
// Fallback if electronAPI unavailable
|
||||
return 'unknown'
|
||||
}
|
||||
Reference in New Issue
Block a user