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
@@ -7,6 +7,7 @@ import { Logo } from '@/components/ui/Logo'
|
||||
import { authApi } from '@/api/client'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { API_BASE_URL, resolveServerPath } from '@/utils/basePath'
|
||||
|
||||
export function LoginPage() {
|
||||
const [username, setUsername] = useState('')
|
||||
@@ -111,7 +112,7 @@ export function LoginPage() {
|
||||
Continue through the configured OpenID Connect provider.
|
||||
</p>
|
||||
<a
|
||||
href={oidcLoginUrl ?? '/api/v1/auth/oidc/login'}
|
||||
href={resolveServerPath(oidcLoginUrl ?? `${API_BASE_URL}/auth/oidc/login`)}
|
||||
className={cn(
|
||||
buttonVariants(),
|
||||
'bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90 font-medium',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { ViewportPortal, useReactFlow, useStore } from '@xyflow/react'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
import { resolveServerPath } from '@/utils/basePath'
|
||||
|
||||
interface ResizeState {
|
||||
startMouseX: number
|
||||
@@ -152,7 +153,7 @@ export function FloorMapLayer() {
|
||||
onDoubleClick={locked ? undefined : (e) => { e.stopPropagation(); requestFloorMapEdit() }}
|
||||
>
|
||||
<img
|
||||
src={imageData}
|
||||
src={resolveServerPath(imageData)}
|
||||
alt="Floor plan"
|
||||
draggable={false}
|
||||
style={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react'
|
||||
import { FloorMapLayer } from '../FloorMapLayer'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
import type { FloorMapConfig } from '@/types'
|
||||
@@ -43,6 +43,16 @@ describe('FloorMapLayer', () => {
|
||||
expect(container.querySelector('img')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders the stored image URL as-is at the root, base64 plans included', () => {
|
||||
setFloorMap()
|
||||
const { rerender } = render(<FloorMapLayer />)
|
||||
expect(screen.getByAltText('Floor plan').getAttribute('src')).toBe('/api/v1/media/abc.png')
|
||||
|
||||
act(() => setFloorMap({ imageData: 'data:image/png;base64,abc' }))
|
||||
rerender(<FloorMapLayer />)
|
||||
expect(screen.getByAltText('Floor plan').getAttribute('src')).toBe('data:image/png;base64,abc')
|
||||
})
|
||||
|
||||
it('hides resize handles until the plan is selected, then shows them (unlocked)', () => {
|
||||
setFloorMap()
|
||||
render(<FloorMapLayer />)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { DESIGN_ICONS, DEFAULT_DESIGN_ICON, resolveDesignIcon } from '@/utils/designIcons'
|
||||
import type { Design, DesignType, FloorMapConfig } from '@/types'
|
||||
import { resolveServerPath } from '@/utils/basePath'
|
||||
|
||||
/** Canvas kinds offered on create. `electrical` shares the network renderer. */
|
||||
const CANVAS_KINDS: { value: DesignType; label: string; hint: string }[] = [
|
||||
@@ -294,7 +295,7 @@ export function DesignModal({
|
||||
) : (
|
||||
<>
|
||||
<div className="relative rounded-lg overflow-hidden border border-[#30363d]" style={{ maxHeight: 160 }}>
|
||||
<img src={imageData} alt="Floor plan preview" className="w-full h-full object-contain" style={{ opacity }} />
|
||||
<img src={resolveServerPath(imageData)} alt="Floor plan preview" className="w-full h-full object-contain" style={{ opacity }} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user