diff --git a/.dockerignore b/.dockerignore index 685fa48..75a34e9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,5 @@ node_modules *.md .github ops -scripts +scripts/* +!scripts/check-node-version.mjs diff --git a/.env.example b/.env.example index 893ed93..7bc99a2 100644 --- a/.env.example +++ b/.env.example @@ -71,6 +71,9 @@ NEXT_PUBLIC_GATEWAY_URL= # Keep gateway auth secrets server-side only (OPENCLAW_GATEWAY_TOKEN / GATEWAY_TOKEN). # Gateway client id used in websocket handshake (role=operator UI client). NEXT_PUBLIC_GATEWAY_CLIENT_ID=openclaw-control-ui +# Gateway optional mode: set to 'true' if deploying on VPS with firewall blocking WebSocket ports. +# In optional mode, Mission Control runs standalone; core CRUD features work but live gateway events do not. +# NEXT_PUBLIC_GATEWAY_OPTIONAL=false # === Data Paths (all optional, defaults to .data/ in project root) === # MISSION_CONTROL_DATA_DIR=.data diff --git a/README.md b/README.md index 7f0fccc..1f16161 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,32 @@ pnpm dev # http://localhost:3000 Initial login is seeded from `AUTH_USER` / `AUTH_PASS` on first run. If `AUTH_PASS` contains `#`, quote it (e.g. `AUTH_PASS="my#password"`) or use `AUTH_PASS_B64`. +## Gateway Optional Mode (Standalone Deployment) + +Mission Control can run in standalone mode without a gateway connection. This is useful when: + +- Deploying on a VPS with firewall rules blocking non-standard WebSocket ports (18789/18790) +- Testing UI/core workflows without a running gateway +- Running Mission Control primarily for project/task operations + +Enable with: + +```bash +NEXT_PUBLIC_GATEWAY_OPTIONAL=true +``` + +When enabled, the HUD status shows `Gateway Optional (Standalone)` instead of `Disconnected`. + +Works without gateway: +- Task board, projects, agents, sessions, scheduler, webhooks, alerts, activity/audit, cost tracking + +Requires active gateway: +- Real-time session updates +- Agent-to-agent messaging +- Gateway log streaming + +For production VPS setups, you can also proxy gateway WebSockets over 443. See `docs/deployment.md`. + ### Docker Hardening (Production) For production deployments, use the hardened compose overlay: diff --git a/docker-compose.yml b/docker-compose.yml index fb53bc9..a0e27ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,12 +21,12 @@ services: - NET_BIND_SERVICE security_opt: - no-new-privileges:true - pids_limit: 256 deploy: resources: limits: memory: 512M cpus: '1.0' + pids: 256 networks: - mc-net restart: unless-stopped diff --git a/docs/deployment.md b/docs/deployment.md index aaa5772..1e63ada 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -175,3 +175,36 @@ Then restart the gateway and reconnect from Mission Control. Device identity signing uses WebCrypto and requires a secure browser context. Open Mission Control over HTTPS (or localhost), then reconnect. + +### "Gateway shows offline on VPS deployment" + +Browser WebSocket connections to non-standard ports (like 18789/18790) are often blocked by VPS firewall/provider rules. + +Quick option: + +```bash +NEXT_PUBLIC_GATEWAY_OPTIONAL=true +``` + +This runs Mission Control in standalone mode (core features available, live gateway streams unavailable). + +Production option: reverse-proxy gateway WebSocket over 443. + +nginx example: + +```nginx +location /gateway-ws { + proxy_pass http://127.0.0.1:18789; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400; +} +``` + +Then point UI to: + +```bash +NEXT_PUBLIC_GATEWAY_URL=wss://your-domain.com/gateway-ws +``` diff --git a/src/components/hud/connection-status.tsx b/src/components/hud/connection-status.tsx index 299e636..4123b0c 100644 --- a/src/components/hud/connection-status.tsx +++ b/src/components/hud/connection-status.tsx @@ -18,10 +18,12 @@ export function ConnectionStatus({ }: ConnectionStatusProps) { const { connection } = useMissionControl() const displayUrl = connection.url || 'ws://:' + const isGatewayOptional = process.env.NEXT_PUBLIC_GATEWAY_OPTIONAL === 'true' const getStatusColor = () => { if (isConnected) return 'bg-green-500 animate-pulse' if (connection.reconnectAttempts > 0) return 'bg-yellow-500' + if (isGatewayOptional && !isConnected) return 'bg-blue-500' return 'bg-red-500' } @@ -32,6 +34,9 @@ export function ConnectionStatus({ if (connection.reconnectAttempts > 0) { return `Reconnecting... (${connection.reconnectAttempts}/10)` } + if (isGatewayOptional && !isConnected) { + return 'Gateway Optional (Standalone)' + } return 'Disconnected' }