feat(deploy): serve Homelable under a configurable base path
Homelable could only own the root of an origin. Behind an existing proxy at `https://home.example/homelab/` the built `index.html` still asked for `/assets/*`, which fell through to whatever owned the root — and when that answered `text/html` for a `<script>` under `nosniff`, the browser failed the load on an HTTP 200. The only workaround was patching the checkout and rebuilding on every upgrade. One build-time knob, `VITE_BASE_PATH`, becomes Vite's `base`. Vite rewrites the asset URLs it emits; everything the app builds by hand goes through the new `utils/basePath.ts` — the axios instances, the WebSocket URL, the live-view route, the local brand icons, and the OIDC login href. `resolveServerPath` covers what the *backend* hands back, which is always root-absolute because it cannot know where the SPA is mounted: uploaded floor-plan URLs already stored in a canvas are resolved at render time, so plans predating the move keep loading. The OIDC callback used to redirect to `/`, dropping subpath users at the origin root after login; it now reads the prefix back out of `OIDC_REDIRECT_URI`. The default is `/`, and stays a no-op there by construction: every helper returns the string it returned before, the root build output is unchanged and both nginx sites are byte-identical to what shipped — the Docker image copies `docker/nginx.conf` verbatim and the installer keeps its original heredoc. Only a non-default prefix takes the generated config. Those generated configs use `root`, never `alias`, since `alias` plus `try_files` mis-resolves `$uri` — the Docker build lands the bundle in the matching subdirectory, and the installer symlinks it under `/var/www/homelable`. Both accept either reverse-proxy style, prefix forwarded intact or already stripped, with no redirect loop between them, and `absolute_redirect off` stops the no-slash 301 from eating the port. Closes #334. ha-relevant: maybe
This commit is contained in:
committed by
Pouzor - Rémy Jardient
parent
3cbda60277
commit
4e8d841807
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useStatusPolling } from '../useStatusPolling'
|
||||
|
||||
// Served under /homelab/ — the WebSocket URL has to carry the prefix too, or it
|
||||
// lands on whatever else owns the root of the origin.
|
||||
vi.mock('@/utils/basePath', () => ({ API_BASE_URL: '/homelab/api/v1' }))
|
||||
|
||||
vi.mock('@/stores/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
setNodeStatus: vi.fn(),
|
||||
notifyScanDeviceFound: vi.fn(),
|
||||
setServiceStatuses: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({ isAuthenticated: true, authMethod: 'local', token: 'test-token' }),
|
||||
}))
|
||||
|
||||
class MockWebSocket {
|
||||
static instances: MockWebSocket[] = []
|
||||
url: string
|
||||
onopen: (() => void) | null = null
|
||||
onmessage: ((e: { data: string }) => void) | null = null
|
||||
onerror: ((e: unknown) => void) | null = null
|
||||
send = vi.fn()
|
||||
close = vi.fn()
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url
|
||||
MockWebSocket.instances.push(this)
|
||||
}
|
||||
}
|
||||
|
||||
describe('useStatusPolling under a base path', () => {
|
||||
beforeEach(() => {
|
||||
MockWebSocket.instances = []
|
||||
vi.stubGlobal('WebSocket', MockWebSocket)
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { protocol: 'http:', host: 'home.example' },
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('prefixes the WebSocket URL with the base path', () => {
|
||||
renderHook(() => useStatusPolling())
|
||||
expect(MockWebSocket.instances).toHaveLength(1)
|
||||
expect(MockWebSocket.instances[0].url).toBe('ws://home.example/homelab/api/v1/status/ws/status')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user