From 91df6de4d491f82dd2278f152b8b87b6110991c4 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sat, 20 Dec 2025 02:35:34 +0100
Subject: [PATCH] Add browser support with Socket.io transport, authentication,
performance-optimized IPC, and CI/CD (#925)
---
.devcontainer/devcontainer.json | 46 ++
.devcontainer/docker-compose.yml | 21 +
.devcontainer/mosquitto.conf | 4 +
.github/copilot-instructions.md | 6 +-
.github/workflows/copilot-setup.yml | 2 +-
.github/workflows/tests.yml | 50 ++
.gitignore | 2 +-
BROWSER_MODE.md | 193 +++++
CI_CD.md | 149 ++++
Readme.md | 39 ++
app/package.json | 10 +-
app/src/components/BrowserAuthWrapper.tsx | 65 ++
.../BrowserCertificateFileSelection.tsx | 140 ++++
.../ConnectionSetup/Certificates.tsx | 13 +-
app/src/components/LoginDialog.tsx | 57 ++
app/src/components/UpdateNotifier.tsx | 14 +-
app/src/index.tsx | 5 +-
app/src/mocks/electron.ts | 12 +
app/webpack.browser.config.js | 102 +++
app/yarn.lock | 359 ++++++----
backend/package.json | 2 +-
backend/src/ConfigStorage.ts | 13 +-
backend/src/index.ts | 19 +-
backend/yarn.lock | 93 ---
events/EventSystem/BrowserEventBus.ts | 29 +
events/EventSystem/IpcMainEventBus.ts | 80 ++-
events/EventSystem/IpcMainEventBusV2.ts | 61 ++
events/EventSystem/IpcRendererEventBusV2.ts | 65 ++
events/EventSystem/MessageCodec.ts | 74 ++
events/EventSystem/SocketIOClientEventBus.ts | 42 ++
events/EventSystem/SocketIOServerEventBus.ts | 274 ++++++++
events/EventsV2.ts | 71 ++
events/OpenDialogRequest.ts | 1 +
events/index.ts | 1 +
package.json | 19 +-
src/AuthManager.ts | 94 +++
src/electron.ts | 25 +-
src/server.ts | 177 +++++
src/spec/demoVideo.ts | 2 +-
src/spec/leakTest.ts | 2 +-
tsconfig.json | 2 +
yarn.lock | 660 +++++++++++++++++-
42 files changed, 2805 insertions(+), 290 deletions(-)
create mode 100644 .devcontainer/devcontainer.json
create mode 100644 .devcontainer/docker-compose.yml
create mode 100644 .devcontainer/mosquitto.conf
create mode 100644 BROWSER_MODE.md
create mode 100644 CI_CD.md
create mode 100644 app/src/components/BrowserAuthWrapper.tsx
create mode 100644 app/src/components/ConnectionSetup/BrowserCertificateFileSelection.tsx
create mode 100644 app/src/components/LoginDialog.tsx
create mode 100644 app/src/mocks/electron.ts
create mode 100644 app/webpack.browser.config.js
create mode 100644 events/EventSystem/BrowserEventBus.ts
create mode 100644 events/EventSystem/IpcMainEventBusV2.ts
create mode 100644 events/EventSystem/IpcRendererEventBusV2.ts
create mode 100644 events/EventSystem/MessageCodec.ts
create mode 100644 events/EventSystem/SocketIOClientEventBus.ts
create mode 100644 events/EventSystem/SocketIOServerEventBus.ts
create mode 100644 events/EventsV2.ts
create mode 100644 src/AuthManager.ts
create mode 100644 src/server.ts
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..f799096
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,46 @@
+{
+ "name": "MQTT Explorer Development",
+ "dockerComposeFile": "docker-compose.yml",
+ "service": "app",
+ "workspaceFolder": "/workspace",
+
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "ms-vscode.vscode-typescript-next",
+ "ms-azuretools.vscode-docker",
+ "eamodio.gitlens"
+ ],
+ "settings": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit"
+ }
+ }
+ }
+ },
+
+ "forwardPorts": [3000, 8080, 1883],
+ "portsAttributes": {
+ "3000": {
+ "label": "MQTT Explorer Server",
+ "onAutoForward": "notify"
+ },
+ "8080": {
+ "label": "Webpack Dev Server",
+ "onAutoForward": "notify"
+ },
+ "1883": {
+ "label": "MQTT Broker",
+ "onAutoForward": "ignore"
+ }
+ },
+
+ "postCreateCommand": "yarn install",
+
+ "remoteUser": "node"
+}
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
new file mode 100644
index 0000000..0de4449
--- /dev/null
+++ b/.devcontainer/docker-compose.yml
@@ -0,0 +1,21 @@
+version: '3.8'
+
+services:
+ app:
+ image: mcr.microsoft.com/devcontainers/javascript-node:20
+ volumes:
+ - ../..:/workspace:cached
+ command: sleep infinity
+ network_mode: service:mosquitto
+ environment:
+ - MQTT_EXPLORER_USERNAME=dev
+ - MQTT_EXPLORER_PASSWORD=dev123
+
+ mosquitto:
+ image: eclipse-mosquitto:2
+ ports:
+ - "1883:1883"
+ - "3000:3000"
+ - "8080:8080"
+ volumes:
+ - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
diff --git a/.devcontainer/mosquitto.conf b/.devcontainer/mosquitto.conf
new file mode 100644
index 0000000..e89e8fc
--- /dev/null
+++ b/.devcontainer/mosquitto.conf
@@ -0,0 +1,4 @@
+# Mosquitto configuration for development
+listener 1883
+allow_anonymous true
+persistence false
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index ee88bab..79f6c0c 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -25,6 +25,10 @@ yarn install
# Build the project
yarn build
+# Set password for browser testing
+export MQTT_EXPLORER_USERNAME=admin
+export MQTT_EXPLORER_PASSWORD=secretpassword
+
# Start the application
yarn start
@@ -322,5 +326,5 @@ yarn package-with-docker
- The app uses Electron (see `package.json` for version)
- MQTT communication is handled via [mqttjs](https://github.com/mqttjs/MQTT.js)
- All code changes should pass linting (`yarn lint`)
-- Node.js version requirement: >= 18
+- Node.js version requirement: >= 20
- The project uses workspace-like structure with separate package.json files for app and backend
diff --git a/.github/workflows/copilot-setup.yml b/.github/workflows/copilot-setup.yml
index e56a02d..007f557 100644
--- a/.github/workflows/copilot-setup.yml
+++ b/.github/workflows/copilot-setup.yml
@@ -14,7 +14,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: '18'
+ node-version: '20'
- name: Get yarn cache directory path
id: yarn-cache-dir-path
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4709545..45bdc21 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -74,3 +74,53 @@ jobs:
run: echo '${{ steps.upload.outputs.file-url }}'
id: artifact-upload-step
- run: echo '
' >> $GITHUB_STEP_SUMMARY
+
+ test-browser:
+ runs-on: ubuntu-latest
+ services:
+ mosquitto:
+ image: eclipse-mosquitto:2
+ ports:
+ - 1883:1883
+ options: >-
+ --health-cmd "mosquitto_sub -t '$SYS/#' -C 1"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ - name: Install Dependencies
+ run: yarn install --frozen-lockfile
+ - name: Build Browser Mode
+ run: yarn build:server
+ - name: Test App
+ run: yarn test:app
+ - name: Test Backend
+ run: yarn test:backend
+ - name: Start Server in Background
+ run: |
+ yarn start:server &
+ echo $! > server.pid
+ env:
+ MQTT_EXPLORER_USERNAME: test
+ MQTT_EXPLORER_PASSWORD: test123
+ PORT: 3000
+ - name: Wait for Server
+ run: |
+ timeout 30 bash -c 'until curl -f http://localhost:3000; do sleep 1; done'
+ - name: Browser Smoke Test
+ run: |
+ # Test server is running
+ curl -f http://localhost:3000 || exit 1
+ echo "Browser mode server is running successfully"
+ - name: Stop Server
+ if: always()
+ run: |
+ if [ -f server.pid ]; then
+ kill $(cat server.pid) || true
+ rm server.pid
+ fi
diff --git a/.gitignore b/.gitignore
index 312530e..d12aede 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,5 +15,5 @@ mqtt-explorer-mcp-screenshot.png
screenshot-mcp-*.png
test-mcp-introspection.js
-# UI test artifacts
+/data
test-screenshot-*.png
diff --git a/BROWSER_MODE.md b/BROWSER_MODE.md
new file mode 100644
index 0000000..c0c3af2
--- /dev/null
+++ b/BROWSER_MODE.md
@@ -0,0 +1,193 @@
+# Browser Mode Documentation
+
+MQTT Explorer now supports running as a web application served by a Node.js server, in addition to the existing Electron desktop app.
+
+## Running in Browser Mode
+
+### Quick Start
+
+1. Build the application for browser mode:
+ ```bash
+ yarn build:server
+ ```
+
+2. Start the server:
+ ```bash
+ yarn start:server
+ ```
+
+3. Open your browser and navigate to `http://localhost:3000`
+
+4. You'll be prompted to log in with credentials that were generated on server startup.
+
+### Development Mode
+
+To run in development mode with hot reload:
+
+```bash
+yarn dev:server
+```
+
+This starts both the webpack dev server and the backend server.
+
+## Authentication
+
+### Environment Variables
+
+You can set custom authentication credentials using environment variables:
+
+```bash
+export MQTT_EXPLORER_USERNAME=admin
+export MQTT_EXPLORER_PASSWORD=secretpassword
+yarn start:server
+```
+
+### Generated Credentials
+
+If no environment variables are set, the server will generate credentials on first startup and save them to `data/credentials.json`. The generated credentials will be printed to the console:
+
+```
+============================================================
+Generated new credentials:
+Username: user-abc123
+Password: 123e4567-e89b-12d3-a456-426614174000
+============================================================
+Please save these credentials. They will be persisted to:
+/path/to/data/credentials.json
+============================================================
+```
+
+## Features
+
+### Certificate Upload
+
+In browser mode, certificate files are uploaded directly through the browser using the HTML5 File API. The certificates are:
+- Read client-side as base64
+- Stored in the connection configuration
+- Used when establishing MQTT connections
+
+### Data Storage
+
+In browser mode, all data is stored on the server:
+- Credentials: `data/credentials.json`
+- Uploaded certificates: `data/certificates/`
+- File uploads: `data/uploads/`
+
+### Port Configuration
+
+The default port is 3000. You can change it using the `PORT` environment variable:
+
+```bash
+PORT=8080 yarn start:server
+```
+
+## Architecture
+
+### Client-Server Communication
+
+- **Electron Mode**: Uses Electron IPC for communication between renderer and main process
+- **Browser Mode**: Uses Socket.io WebSockets for real-time communication between browser and server
+
+The application automatically detects the environment and uses the appropriate transport layer.
+
+### Event Bus Abstraction
+
+Both Electron IPC and Socket.io implement the same `EventBusInterface`, allowing the application code to work seamlessly in both modes without modification.
+
+## Differences from Electron Mode
+
+### Browser Mode Limitations
+
+1. **File System Access**: Limited to server-side operations
+2. **Native Dialogs**: File selection uses browser file input instead of native dialogs
+3. **Auto-Updates**: Not available in browser mode
+4. **Tray Icon**: Not available in browser mode
+
+### Browser Mode Advantages
+
+1. **No Installation**: Access from any browser
+2. **Cross-Platform**: Works on any device with a modern browser
+3. **Remote Access**: Can be deployed on a server for remote access
+4. **Multi-User**: Can support authentication for multiple users
+
+## Security Considerations
+
+1. **HTTPS**: For production, always use HTTPS to encrypt credentials and MQTT data
+2. **Authentication**: Keep credentials secure and rotate them regularly
+3. **Network**: Ensure the server is on a trusted network or behind a firewall
+4. **Environment Variables**: Use environment variables for production credentials, not the generated ones
+
+## Deployment
+
+For production deployment:
+
+1. Build the application:
+ ```bash
+ yarn build:server
+ ```
+
+2. Set environment variables:
+ ```bash
+ export MQTT_EXPLORER_USERNAME=your_username
+ export MQTT_EXPLORER_PASSWORD=your_secure_password
+ export PORT=3000
+ ```
+
+3. Start the server:
+ ```bash
+ yarn start:server
+ ```
+
+4. Use a reverse proxy (nginx, Apache) to add HTTPS and additional security features
+
+## Troubleshooting
+
+### Debugging
+
+Enable detailed Socket.IO connection and lifecycle debugging:
+
+```bash
+DEBUG=mqtt-explorer:socketio* yarn start:server
+```
+
+Available debug namespaces:
+- `mqtt-explorer:socketio` - General Socket.IO events and metrics
+- `mqtt-explorer:socketio:connect` - Client connection events
+- `mqtt-explorer:socketio:disconnect` - Client disconnection and cleanup
+- `mqtt-explorer:socketio:subscriptions` - Subscription lifecycle tracking
+- `mqtt-explorer:socketio:connections` - MQTT connection ownership
+
+This will log:
+- Client connect/disconnect events
+- Subscription counts per socket
+- MQTT connection ownership tracking
+- Memory leak detection metrics (subscriptions, handlers, connections)
+
+Example output:
+```
+mqtt-explorer:socketio:connect Client connected: abc123de
+mqtt-explorer:socketio [connect] clients=1 subscriptions=8 mqttConns=0 | socket[abc123de]: subs=8 conns=0
+mqtt-explorer:socketio:connections Connection my-mqtt owned by socket abc123de (total: 1)
+mqtt-explorer:socketio:disconnect Client disconnected: abc123de
+mqtt-explorer:socketio:subscriptions Removed 8 subscriptions for socket abc123de
+mqtt-explorer:socketio [disconnect] clients=0 subscriptions=0 mqttConns=0 | socket[abc123de]: subs=0 conns=0
+```
+
+### Authentication Fails
+
+1. Check the console output for the generated credentials
+2. Clear browser session storage: `sessionStorage.clear()` in browser console
+3. Restart the server to regenerate credentials
+
+### Connection Issues
+
+1. Check that the server is running: `http://localhost:3000`
+2. Check browser console for Socket.io connection errors
+3. Verify firewall rules allow the port
+
+### Certificate Upload Issues
+
+In browser mode, certificates are handled differently:
+- Use the file upload button to select certificate files
+- Files are read and encoded client-side
+- Large certificate files (>16KB) will be rejected
diff --git a/CI_CD.md b/CI_CD.md
new file mode 100644
index 0000000..cf9dea8
--- /dev/null
+++ b/CI_CD.md
@@ -0,0 +1,149 @@
+# CI/CD Pipeline Documentation
+
+## Overview
+
+MQTT Explorer uses GitHub Actions for continuous integration and testing. The pipeline tests both Electron (desktop) and browser modes.
+
+## Workflows
+
+### Test Workflow (`.github/workflows/tests.yml`)
+
+This workflow runs on pull requests to `master`, `beta`, and `release` branches.
+
+#### Jobs
+
+##### 1. `test` - Electron Mode Tests
+
+Tests the traditional Electron desktop application:
+
+- **Environment**: Custom Docker container (`ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest`)
+- **Steps**:
+ 1. Install dependencies with frozen lockfile
+ 2. Build the Electron application
+ 3. Run unit tests (app + backend)
+ 4. Run UI tests with video recording
+ 5. Upload test video to S3
+ 6. Display test results in GitHub summary
+
+**Artifacts**: UI test video (GIF format) uploaded to S3
+
+##### 2. `test-browser` - Browser Mode Tests
+
+Tests the new browser/server mode:
+
+- **Environment**: Ubuntu latest with Node.js 20
+- **Services**:
+ - **Mosquitto MQTT Broker**: Eclipse Mosquitto v2 on port 1883
+ - Health checks enabled
+ - Anonymous connections allowed
+- **Steps**:
+ 1. Setup Node.js 20
+ 2. Install dependencies
+ 3. Build browser mode (`yarn build:server`)
+ 4. Run unit tests (app + backend)
+ 5. Start server in background with test credentials
+ 6. Wait for server to be ready
+ 7. Run browser smoke tests
+ 8. Clean up server process
+
+**Environment Variables**:
+- `MQTT_EXPLORER_USERNAME=test`
+- `MQTT_EXPLORER_PASSWORD=test123`
+- `PORT=3000`
+
+## Test Commands
+
+The following npm scripts are used in CI/CD:
+
+```bash
+# Unit tests
+yarn test # Run all tests (app + backend)
+yarn test:app # Frontend tests only
+yarn test:backend # Backend tests only
+
+# Build
+yarn build # Build Electron mode
+yarn build:server # Build browser mode
+
+# UI Tests (Electron only)
+yarn ui-test # Run UI tests with video recording
+```
+
+## Adding New Tests
+
+### For Electron Mode
+
+Add tests to the `test` job. UI tests should be added to the test suite that `yarn ui-test` runs.
+
+### For Browser Mode
+
+Browser-specific tests should:
+1. Use the pre-configured Mosquitto service
+2. Connect to `mqtt://mosquitto:1883`
+3. Test server endpoints at `http://localhost:3000`
+
+Example:
+```yaml
+- name: Browser Integration Test
+ run: |
+ # Test MQTT connection through server
+ curl -X POST http://localhost:3000/api/test
+```
+
+## Local Testing
+
+### Electron Mode
+
+```bash
+yarn build
+yarn test
+yarn ui-test
+```
+
+### Browser Mode
+
+```bash
+# Start Mosquitto in Docker
+docker run -d -p 1883:1883 eclipse-mosquitto:2
+
+# Build and test
+yarn build:server
+yarn test
+
+# Start server
+MQTT_EXPLORER_USERNAME=test MQTT_EXPLORER_PASSWORD=test123 yarn start:server
+
+# Run manual tests
+curl http://localhost:3000
+```
+
+## GitHub Codespaces / Devcontainer
+
+The repository includes a devcontainer configuration that automatically sets up:
+- Node.js 20
+- MQTT broker (Mosquitto)
+- All development dependencies
+- Port forwarding for development
+
+See [.devcontainer/README.md](.devcontainer/README.md) for details.
+
+## Troubleshooting
+
+### Browser Tests Failing
+
+1. **Server won't start**: Check if port 3000 is already in use
+2. **MQTT connection fails**: Ensure Mosquitto service is healthy
+3. **Timeout errors**: Increase timeout in "Wait for Server" step
+
+### Electron Tests Failing
+
+1. **UI tests timeout**: Check if the Docker container has display access
+2. **Build fails**: Verify all dependencies are in yarn.lock
+
+## Future Improvements
+
+- [ ] Add E2E browser tests with Playwright
+- [ ] Test WebSocket connections in browser mode
+- [ ] Add performance benchmarks
+- [ ] Test with different MQTT broker versions
+- [ ] Add security scanning for browser mode
diff --git a/Readme.md b/Readme.md
index 4a5e0aa..9b7720f 100644
--- a/Readme.md
+++ b/Readme.md
@@ -18,8 +18,22 @@ Downloads can be found at the link above.
This page is dedicated to its development.
Pull-Requests and error reports are welcome.
+## Quick Start with GitHub Codespaces
+
+The fastest way to start developing is with GitHub Codespaces:
+
+1. Click the green "Code" button above
+2. Select "Codespaces" tab
+3. Click "Create codespace on [branch]"
+4. Wait for the environment to set up (includes Node.js and MQTT broker)
+5. Run `yarn dev:server` to start development
+
+The devcontainer includes a pre-configured MQTT broker and all development tools. See [.devcontainer/README.md](.devcontainer/README.md) for details.
+
## Run from sources
+### Desktop Application (Electron)
+
```bash
npm install -g yarn
yarn
@@ -27,8 +41,23 @@ yarn build
yarn start
```
+### Browser Mode (Web Application)
+
+MQTT Explorer can also run as a web application served by a Node.js server:
+
+```bash
+npm install -g yarn
+yarn
+yarn build:server
+yarn start:server
+```
+
+Then open your browser to `http://localhost:3000`. For more details, see [BROWSER_MODE.md](BROWSER_MODE.md).
+
## Develop
+### Desktop Application
+
Launch Application
```bash
@@ -37,6 +66,16 @@ yarn
yarn dev
```
+### Browser Mode
+
+Launch in development mode with hot reload:
+
+```bash
+npm install -g yarn
+yarn
+yarn dev:server
+```
+
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management, `src` contains all the electron bindings. [mqttjs](https://github.com/mqttjs/MQTT.js) is used to facilitate communication to MQTT brokers.
## Automated Tests
diff --git a/app/package.json b/app/package.json
index bd1256b..aff9980 100644
--- a/app/package.json
+++ b/app/package.json
@@ -10,7 +10,7 @@
"mochatest": "mocha --require ts-node/register --require source-map-support/register --recursive src/*/**/*.spec.ts"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
},
"author": "",
"license": "CC-BY-ND-4.0",
@@ -28,6 +28,7 @@
"d3-shape": "^1.3.5",
"diff": "^4.0.1",
"dot-prop": "^5.0.0",
+ "events": "^3.3.0",
"get-value": "^3.0.1",
"immutable": "^4.0.0-rc.12",
"in-viewport": "^3.6.0",
@@ -37,7 +38,9 @@
"lodash.throttle": "^4.1.1",
"moving-average": "^1.0.0",
"number-abbreviate": "^2.0.0",
+ "os-browserify": "^0.3.0",
"parse-duration": "^0.1.1",
+ "path-browserify": "^1.0.1",
"prismjs": "^1.15.0",
"react": "^16.11",
"react-ace": "^8",
@@ -51,7 +54,8 @@
"redux-batched-actions": "0.5",
"redux-thunk": "^2.3.0",
"sha1": "^1.1.1",
- "socket.io-client": "^2.2.0",
+ "socket.io-client": "^4.8.1",
+ "url": "^0.11.4",
"uuid": "7"
},
"devDependencies": {
@@ -66,7 +70,7 @@
"@types/react-redux": "^7.0.9",
"@types/react-resize-detector": "^4.0.1",
"@types/sha1": "^1.1.1",
- "@types/socket.io-client": "^1.4.32",
+ "@types/socket.io-client": "^3.0.0",
"@types/uuid": "^7.0.2",
"@types/vis": "^4.21.9",
"chai": "^4.2.0",
diff --git a/app/src/components/BrowserAuthWrapper.tsx b/app/src/components/BrowserAuthWrapper.tsx
new file mode 100644
index 0000000..a9e5d90
--- /dev/null
+++ b/app/src/components/BrowserAuthWrapper.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react'
+import { LoginDialog } from './LoginDialog'
+
+interface BrowserAuthWrapperProps {
+ children: React.ReactNode
+}
+
+const isBrowserMode =
+ typeof window !== 'undefined' &&
+ (typeof process === 'undefined' || process.env?.BROWSER_MODE === 'true')
+
+export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
+ const [isAuthenticated, setIsAuthenticated] = React.useState(false)
+ const [loginError, setLoginError] = React.useState()
+ const [showLogin, setShowLogin] = React.useState(false)
+
+ React.useEffect(() => {
+ if (!isBrowserMode) {
+ // Not in browser mode, skip authentication
+ setIsAuthenticated(true)
+ return
+ }
+
+ // Check if already authenticated
+ const username = sessionStorage.getItem('mqtt-explorer-username')
+ const password = sessionStorage.getItem('mqtt-explorer-password')
+
+ if (username && password) {
+ // Try to use stored credentials
+ setIsAuthenticated(true)
+ } else {
+ // Show login dialog
+ setShowLogin(true)
+ }
+ }, [])
+
+ const handleLogin = async (username: string, password: string) => {
+ try {
+ // Store credentials in session storage
+ sessionStorage.setItem('mqtt-explorer-username', username)
+ sessionStorage.setItem('mqtt-explorer-password', password)
+
+ // The socket will use these credentials on next connection
+ setIsAuthenticated(true)
+ setShowLogin(false)
+ setLoginError(undefined)
+
+ // Reload to reinitialize socket with new auth
+ window.location.reload()
+ } catch (error) {
+ setLoginError('Login failed. Please check your credentials.')
+ }
+ }
+
+ if (!isBrowserMode) {
+ // Not in browser mode, render children directly
+ return <>{props.children}>
+ }
+
+ if (!isAuthenticated) {
+ return
+ }
+
+ return <>{props.children}>
+}
diff --git a/app/src/components/ConnectionSetup/BrowserCertificateFileSelection.tsx b/app/src/components/ConnectionSetup/BrowserCertificateFileSelection.tsx
new file mode 100644
index 0000000..d1b30ad
--- /dev/null
+++ b/app/src/components/ConnectionSetup/BrowserCertificateFileSelection.tsx
@@ -0,0 +1,140 @@
+import * as React from 'react'
+import ClearAdornment from '../helper/ClearAdornment'
+import Lock from '@material-ui/icons/Lock'
+import { bindActionCreators } from 'redux'
+import { Button, Theme, Tooltip, Typography } from '@material-ui/core'
+import { CertificateParameters, ConnectionOptions } from '../../model/ConnectionOptions'
+import { CertificateTypes } from '../../actions/ConnectionManager'
+import { connect } from 'react-redux'
+import { connectionManagerActions } from '../../actions'
+import { withStyles } from '@material-ui/styles'
+import { rendererRpc } from '../../../../events'
+import { RpcEvents } from '../../../../events/EventsV2'
+
+function BrowserCertificateFileSelection(props: {
+ certificateType: CertificateTypes
+ title: string
+ certificate?: CertificateParameters
+ classes: any
+ actions: {
+ connectionManager: typeof connectionManagerActions
+ }
+ connection: ConnectionOptions
+}) {
+ const fileInputRef = React.useRef(null)
+
+ const clearCertificate = React.useCallback(() => {
+ props.actions.connectionManager.updateConnection(props.connection.id, {
+ [props.certificateType]: undefined,
+ })
+ }, [props.connection, props.certificateType])
+
+ const handleFileSelect = React.useCallback(
+ async (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0]
+ if (!file) {
+ return
+ }
+
+ try {
+ // Read file content
+ const reader = new FileReader()
+ reader.onload = async e => {
+ const content = e.target?.result
+ if (typeof content === 'string') {
+ // Convert to base64
+ const base64Data = content.split(',')[1] || content
+
+ // Upload via IPC instead of HTTP POST
+ const result = await rendererRpc.call(RpcEvents.uploadCertificate, {
+ filename: file.name,
+ data: base64Data,
+ })
+
+ // Create certificate parameters
+ const certificate: CertificateParameters = {
+ name: result.name,
+ data: result.data,
+ }
+
+ // Update connection
+ props.actions.connectionManager.updateConnection(props.connection.id, {
+ [props.certificateType]: certificate,
+ })
+ }
+ }
+ reader.readAsDataURL(file)
+ } catch (error) {
+ console.error('Error uploading certificate:', error)
+ }
+
+ // Reset input
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''
+ }
+ },
+ [props.connection.id, props.certificateType, props.actions.connectionManager]
+ )
+
+ const handleButtonClick = () => {
+ fileInputRef.current?.click()
+ }
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+function ClearCertificate(props: { classes: any; certificate?: CertificateParameters; action: () => void }) {
+ if (!props.certificate) {
+ return null
+ }
+
+ return (
+
+
+
+ {props.certificate.name}
+
+
+ )
+}
+
+const mapDispatchToProps = (dispatch: any) => {
+ return {
+ actions: {
+ connectionManager: bindActionCreators(connectionManagerActions, dispatch),
+ },
+ }
+}
+
+const styles = (theme: Theme) => ({
+ certificateName: {
+ width: '100%',
+ height: 'calc(1em + 4px)',
+ overflow: 'hidden' as 'hidden',
+ whiteSpace: 'nowrap' as 'nowrap',
+ textOverflow: 'ellipsis' as 'ellipsis',
+ color: theme.palette.text.hint,
+ },
+ button: {
+ marginTop: theme.spacing(3),
+ marginRight: theme.spacing(2),
+ },
+})
+
+export default connect(undefined, mapDispatchToProps)(withStyles(styles)(BrowserCertificateFileSelection))
diff --git a/app/src/components/ConnectionSetup/Certificates.tsx b/app/src/components/ConnectionSetup/Certificates.tsx
index 0277c1a..f72cdeb 100644
--- a/app/src/components/ConnectionSetup/Certificates.tsx
+++ b/app/src/components/ConnectionSetup/Certificates.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
import CertificateFileSelection from './CertificateFileSelection'
+import BrowserCertificateFileSelection from './BrowserCertificateFileSelection'
import Undo from '@material-ui/icons/Undo'
import { bindActionCreators } from 'redux'
import { Button, Grid } from '@material-ui/core'
@@ -8,6 +9,12 @@ import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
+// Check if we're in browser mode
+const isBrowserMode =
+ typeof window !== 'undefined' &&
+ (typeof process === 'undefined' || process.env?.BROWSER_MODE === 'true')
+const CertSelector = isBrowserMode ? BrowserCertificateFileSelection : CertificateFileSelection
+
interface Props {
connection: ConnectionOptions
classes: any
@@ -45,7 +52,7 @@ class Certificates extends React.PureComponent {