fix: resolve WebSocket failure and crypto.randomUUID crash on HTTP/LXC

- Replace crypto.randomUUID() with a polyfill (generateUUID) that falls
  back to crypto.getRandomValues or Math.random — fixes crash on HTTP
  non-secure contexts where randomUUID is unavailable
- Fix WebSocket URL hardcoding port 8000 — use window.location.host so
  connections go through Nginx proxy in Docker/LXC instead of bypassing it
- Add /api/v1/status/ws/ location block in nginx.conf with WebSocket
  upgrade headers (must precede /api/ to avoid missing Upgrade header)
This commit is contained in:
Pouzor
2026-03-18 00:16:42 +01:00
parent df3b7a8cb0
commit e5d7260696
6 changed files with 69 additions and 6 deletions
+11 -1
View File
@@ -4,6 +4,16 @@ server {
root /usr/share/nginx/html;
index index.html;
# Proxy WebSocket (must be before /api/ to take priority)
location /api/v1/status/ws/ {
proxy_pass http://backend:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Proxy API to backend
location /api/ {
proxy_pass http://backend:8000;
@@ -11,7 +21,7 @@ server {
proxy_set_header X-Real-IP $remote_addr;
}
# Proxy WebSocket
# Proxy legacy /ws/ path
location /ws/ {
proxy_pass http://backend:8000;
proxy_http_version 1.1;
+3 -2
View File
@@ -2,6 +2,7 @@ import { useEffect, useCallback, useRef, useState } from 'react'
import { ReactFlowProvider, type Connection, type Edge } from '@xyflow/react'
import { type Node } from '@xyflow/react'
import { applyDagreLayout } from '@/utils/layout'
import { generateUUID } from '@/utils/uuid'
import { generateMarkdownTable } from '@/utils/exportMarkdown'
import { exportToPng } from '@/utils/export'
import { TooltipProvider } from '@/components/ui/tooltip'
@@ -238,7 +239,7 @@ export default function App() {
const handleAddNode = useCallback((data: Partial<NodeData>) => {
snapshotHistory()
const id = crypto.randomUUID()
const id = generateUUID()
const isProxmox = data.type === 'proxmox'
const parentNode = data.parent_id ? nodes.find((n) => n.id === data.parent_id) : null
// Children position is relative to parent; place near top-left with padding
@@ -260,7 +261,7 @@ export default function App() {
const handleAddGroupRect = useCallback((data: GroupRectFormData) => {
snapshotHistory()
const id = crypto.randomUUID()
const id = generateUUID()
const newNode: Node<NodeData> = {
id,
type: 'groupRect',
+2 -2
View File
@@ -23,8 +23,8 @@ export function useStatusPolling() {
if (STANDALONE || !isAuthenticated || !token) return
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
const host = window.location.hostname
const url = `${protocol}://${host}:8000/api/v1/status/ws/status?token=${encodeURIComponent(token)}`
const host = window.location.host // includes port when non-standard
const url = `${protocol}://${host}/api/v1/status/ws/status?token=${encodeURIComponent(token)}`
const ws = new WebSocket(url)
wsRef.current = ws
+2 -1
View File
@@ -10,6 +10,7 @@ import {
addEdge,
} from '@xyflow/react'
import type { NodeData, EdgeData } from '@/types'
import { generateUUID } from '@/utils/uuid'
type HistoryEntry = { nodes: Node<NodeData>[]; edges: Edge<EdgeData>[] }
@@ -108,7 +109,7 @@ export const useCanvasStore = create<CanvasState>((set) => ({
if (state.clipboard.length === 0) return state
const newNodes = state.clipboard.map((n) => ({
...n,
id: crypto.randomUUID(),
id: generateUUID(),
position: { x: n.position.x + 50, y: n.position.y + 50 },
selected: false,
parentId: undefined,
+26
View File
@@ -0,0 +1,26 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { generateUUID } from '../uuid'
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
describe('generateUUID', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('returns a valid v4 UUID using crypto.randomUUID when available', () => {
const id = generateUUID()
expect(id).toMatch(UUID_REGEX)
})
it('returns a valid v4 UUID using crypto.getRandomValues fallback', () => {
vi.spyOn(crypto, 'randomUUID' as never).mockImplementation(undefined as never)
const id = generateUUID()
expect(id).toMatch(UUID_REGEX)
})
it('generates unique IDs', () => {
const ids = new Set(Array.from({ length: 100 }, () => generateUUID()))
expect(ids.size).toBe(100)
})
})
+25
View File
@@ -0,0 +1,25 @@
/**
* Generates a UUID v4.
* Falls back to a manual implementation when crypto.randomUUID is unavailable
* (HTTP non-secure contexts, older browsers).
*/
export function generateUUID(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
// Fallback: RFC 4122 v4 UUID using crypto.getRandomValues if available
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
return [...bytes]
.map((b, i) => ([4, 6, 8, 10].includes(i) ? '-' : '') + b.toString(16).padStart(2, '0'))
.join('')
}
// Last resort: Math.random based (not cryptographically secure)
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
}