# OmniRoute Electron Desktop App

This directory contains the Electron desktop application wrapper for OmniRoute.

## Architecture (v1.6.4)

```
electron/
├── main.js          # Main process — window, tray, server lifecycle, CSP, IPC
├── preload.js       # Preload script — secure IPC bridge with disposer pattern
├── package.json     # Electron-specific dependencies & electron-builder config
├── types.d.ts       # TypeScript definitions (AppInfo, ServerStatus, ElectronAPI)
└── assets/          # Application icons and resources

src/shared/hooks/
└── useElectron.ts   # React hooks — useSyncExternalStore, zero re-renders
```

## Key Design Decisions

| Decision                      | Rationale                                                                                        |
| ----------------------------- | ------------------------------------------------------------------------------------------------ |
| `waitForServer()` polling     | Prevents blank screen on cold start — polls `http://localhost:PORT` before loading               |
| `stdio: 'pipe'`               | Captures server stdout/stderr for logging + readiness detection (not `inherit`)                  |
| Disposer pattern              | `onServerStatus()` returns `() => void` for precise listener cleanup (no `removeAllListeners`)   |
| `useSyncExternalStore`        | Zero re-renders for `useIsElectron()` — no `useState` + `useEffect` cycle                        |
| CSP via session headers       | `Content-Security-Policy` restricts `script-src`, `connect-src` etc. per Electron best practices |
| Platform-conditional titlebar | `titleBarStyle: 'hiddenInset'` only on macOS; `default` on Windows/Linux                         |

## Development

### Prerequisites

1. Build the Next.js app first:

```bash
npm run build
```

2. Install Electron dependencies:

```bash
cd electron
npm install
```

### Running in Development

1. Start the Next.js development server:

```bash
npm run dev
```

2. In another terminal, start Electron:

```bash
cd electron
npm run dev
```

### Running in Production Mode

1. Build Next.js in standalone mode:

```bash
npm run build
```

2. Start Electron:

```bash
cd electron
npm start
```

## Building

### Build for Current Platform

```bash
cd electron
npm run build
```

### Build for Specific Platforms

```bash
# Windows
npm run build:win

# macOS (x64 + arm64)
npm run build:mac

# Linux
npm run build:linux
```

## Output

Built applications are placed in `dist-electron/`:

- Windows: `.exe` installer (NSIS) + portable `.exe`
- macOS: `.dmg` installer (Intel + Apple Silicon)
- Linux: `.AppImage`

## Installation

### macOS

1. Download the latest `.dmg` from the [Releases](https://github.com/diegosouzapw/OmniRoute/releases) page.
2. Open the `.dmg` file.
3. Drag `OmniRoute.app` to the Applications folder.
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.

### Linux

1. Download the `.AppImage` from [Releases](https://github.com/diegosouzapw/OmniRoute/releases).
2. Make it executable:
   ```bash
   chmod +x OmniRoute-*.AppImage
   ```
3. Run:
   ```bash
   ./OmniRoute-*.AppImage
   ```

## Features

- **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)                                                                      |
| `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

Place your icons in `assets/`:

- `icon.ico` — Windows icon (256×256)
- `icon.icns` — macOS icon bundle
- `icon.png` — Linux/general use (512×512)
- `tray-icon.png` — System tray icon (16×16 or 32×32)

## IPC Channels

### Invoke (Renderer → Main, async)

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

| Channel           | Description                     |
| ----------------- | ------------------------------- |
| `window-minimize` | Minimize window                 |
| `window-maximize` | Toggle maximize/restore         |
| `window-close`    | Close window (minimize to tray) |

### Receive (Main → Renderer, events)

| Channel         | Payload        | Emitted When                              |
| --------------- | -------------- | ----------------------------------------- |
| `server-status` | `ServerStatus` | Server starts, stops, errors, or restarts |
| `port-changed`  | `number`       | Port change via tray menu                 |

> **Note**: Listeners return disposer functions for precise cleanup. See `useServerStatus` and `usePortChanged` hooks.

## Security

| Feature           | Implementation                                                                  |
| ----------------- | ------------------------------------------------------------------------------- |
| Context Isolation | `contextIsolation: true` — renderer cannot access Node.js                       |
| Node Integration  | `nodeIntegration: false` — no `require()` in renderer                           |
| IPC Whitelist     | Channel names validated in preload via `safeInvoke`/`safeSend`/`safeOn`         |
| URL Validation    | `shell.openExternal()` only allows `http:` / `https:` protocols                 |
| CSP               | `Content-Security-Policy` header set via `session.webRequest.onHeadersReceived` |
| Web Security      | `webSecurity: true` — same-origin policy enforced                               |

## React Hooks

| Hook                   | Returns                         | Description                                      |
| ---------------------- | ------------------------------- | ------------------------------------------------ |
| `useIsElectron()`      | `boolean`                       | Zero-render detection via `useSyncExternalStore` |
| `useElectronAppInfo()` | `{ appInfo, loading, error }`   | App info from main process                       |
| `useDataDir()`         | `{ dataDir, loading, error }`   | User data directory                              |
| `useWindowControls()`  | `{ minimize, maximize, close }` | Window control actions                           |
| `useOpenExternal()`    | `{ openExternal }`              | Open URLs in browser                             |
| `useServerControls()`  | `{ restart, restarting }`       | Server restart control                           |
| `useServerStatus(cb)`  | Disposer                        | Listen for server status events                  |
| `usePortChanged(cb)`   | Disposer                        | Listen for port change events                    |

## Troubleshooting

### App Won't Start

1. Check if port 20128 is available: `lsof -i :20128`
2. Check console logs for `[Electron]` prefix
3. Verify the build output exists in `.build/next/standalone`

### White Screen

1. Verify Next.js build exists — server readiness waits 30s max
2. Check `[Server]` and `[Server:err]` log output
3. Look for CSP violations in developer console

### Build Fails

Ensure you have build tools installed:

- Windows: Visual Studio Build Tools
- macOS: Xcode Command Line Tools
- Linux: `build-essential`, `libsecret-1-dev`

## License

MIT
