Skip to content

Commit 35ce1f2

Browse files
committed
Update Dockerfile and client files for Home Assistant Ingress compatibility
- Added ENV variable `VITE_BASE` in Dockerfile to set relative base for asset loading. - Updated asset links in `client/index.html` to use relative paths. - Modified `vite.config.ts` to handle relative base for Home Assistant Ingress. - Refactored asset URL handling in `App.tsx`, `UserGuideModal.tsx`, and `api.ts` to utilize new utility functions for public asset URLs. - Adjusted socket connection path in `useSocket.ts` to ensure compatibility with the new base URL structure.
1 parent a3c4622 commit 35ce1f2

9 files changed

Lines changed: 68 additions & 13 deletions

File tree

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ ARG BUILD_VERSION=1.0.2
4545
ARG VITE_APP_BUILD_VERSION=
4646
ARG VITE_INSTALL_KIND=docker
4747
ENV VITE_INSTALL_KIND=docker
48+
# Relative Vite base so JS/CSS load under Home Assistant Ingress (/hassio/ingress/<slug>/), not from site root.
49+
ENV VITE_BASE=./
4850
RUN export VITE_APP_BUILD_VERSION="${VITE_APP_BUILD_VERSION:-$BUILD_VERSION}" \
4951
&& npm run build -w client
5052
RUN npm run build -w server

client/index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
<meta charset="UTF-8" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<meta name="theme-color" content="#1e4d5e" />
8-
<link rel="icon" href="/favicon.ico" sizes="any" />
9-
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
10-
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
11-
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
12-
<link rel="mask-icon" href="/mask-icon.svg" color="#1e4d5e" />
8+
<link rel="icon" href="./favicon.ico" sizes="any" />
9+
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png" />
10+
<link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png" />
11+
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
12+
<link rel="mask-icon" href="./mask-icon.svg" color="#1e4d5e" />
1313
<title>Financial Overview</title>
1414
</head>
1515

client/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { usePersonaSetupWizardVisibility } from './hooks/usePersonaSetupWizardVi
3030
import { transactionsForReviewItems, transactionNeedsReview, type TransactionReviewItem } from '@app/shared';
3131
import { useEnvConfig } from './hooks/useConfig';
3232
import { isGeminiApiKeyConfigured } from './utils/geminiKeyConfigured';
33+
import { publicAssetUrl } from './utils/publicBase';
3334

3435
function consumeSessionConfigTab(): string | null {
3536
try {
@@ -286,7 +287,7 @@ function App() {
286287
)}
287288
<button
288289
type="button"
289-
onClick={() => window.open(`${import.meta.env.BASE_URL}GUIDE.html`, '_blank')}
290+
onClick={() => window.open(publicAssetUrl('GUIDE.html'), '_blank')}
290291
className="h-9 w-9 inline-flex items-center justify-center rounded-full text-gray-500 hover:text-emerald-800 hover:bg-emerald-50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
291292
title={t('common.help_docs', 'Help Docs')}
292293
aria-label={t('common.help_docs', 'Help Docs')}

client/src/components/UserGuideModal.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState } from 'react';
22
import { useTranslation } from 'react-i18next';
3+
import { publicAssetUrl } from '../utils/publicBase';
34

45
interface UserGuideModalProps {
56
isOpen: boolean;
@@ -318,7 +319,7 @@ export function UserGuideModal({ isOpen, onClose }: UserGuideModalProps) {
318319
{/* Footer Buttons */}
319320
<div className="bg-gray-50 border-t border-gray-200 p-4 flex justify-between gap-3">
320321
<a
321-
href={`${import.meta.env.BASE_URL}GUIDE.html`}
322+
href={publicAssetUrl('GUIDE.html')}
322323
target="_blank"
323324
rel="noopener noreferrer"
324325
className="px-4 py-2 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"

client/src/hooks/useSocket.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback } from 'react';
22
import { io, Socket } from 'socket.io-client';
33
import type { TransactionReviewItem } from '@app/shared';
44
import { api } from '../lib/api';
5+
import { getSocketIoPath } from '../utils/publicBase';
56
import { isDemoMode } from '../demo/isDemo';
67

78
export interface ScrapeProgress {
@@ -31,8 +32,6 @@ export interface TransactionReviewNeededEvent {
3132
items: TransactionReviewItem[];
3233
}
3334

34-
const SOCKET_URL = '';
35-
3635
export function useSocket() {
3736
const [socket, setSocket] = useState<Socket | null>(null);
3837
const [isConnected, setIsConnected] = useState(false);
@@ -63,7 +62,8 @@ export function useSocket() {
6362

6463
useEffect(() => {
6564
if (isDemoMode()) return;
66-
const socketInstance = io(SOCKET_URL, {
65+
const socketInstance = io('', {
66+
path: getSocketIoPath(),
6767
transports: ['websocket', 'polling'],
6868
}) as any;
6969

client/src/lib/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import axios from 'axios';
2+
import { getResolvedPublicBase } from '../utils/publicBase';
23

34
/**
45
* Root URL for the REST API (no trailing slash). In production, respects Vite `BASE_URL` (GitHub Pages project sites).
@@ -14,7 +15,7 @@ export function getApiRoot(): string {
1415

1516
/** Google OAuth redirect URI shown in UI and used with the real backend. */
1617
export function getGoogleOAuthCallbackUrl(): string {
17-
return new URL('api/auth/google/callback', window.location.origin + import.meta.env.BASE_URL).href;
18+
return new URL('api/auth/google/callback', getResolvedPublicBase()).href;
1819
}
1920

2021
const getAxiosBaseUrl = () => getApiRoot();

client/src/main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import { OnboardingProvider } from './contexts/OnboardingContext';
88
import { GettingStartedProvider } from './contexts/GettingStartedContext';
99
import { ServerActivityProvider } from './contexts/ServerActivityContext';
1010
import { isDemoMode } from './demo/isDemo';
11+
import { getResolvedPublicBase } from './utils/publicBase';
1112

1213
async function bootstrap() {
1314
if (isDemoMode()) {
1415
const { worker } = await import('./demo/browser');
1516
await worker.start({
1617
onUnhandledRequest: 'bypass',
1718
serviceWorker: {
18-
// BASE_URL is a path (/ or /repo/); URL() needs an absolute base.
19-
url: new URL('mockServiceWorker.js', `${window.location.origin}${import.meta.env.BASE_URL}`).href,
19+
url: new URL('mockServiceWorker.js', getResolvedPublicBase()).href,
2020
},
2121
});
2222
}

client/src/utils/publicBase.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Vite `base: './'` is used for Home Assistant Ingress (and other subpath hosts) so asset URLs are
3+
* relative to the page. Helpers here resolve same-origin links that would break if built with `base: '/'`.
4+
*/
5+
6+
export function isIngressRelativeBase(): boolean {
7+
const b = import.meta.env.BASE_URL;
8+
return b === './' || b.startsWith('./');
9+
}
10+
11+
/** Directory URL (with trailing slash) for resolving static files and API roots in the browser. */
12+
export function getResolvedPublicBase(): string {
13+
const base = import.meta.env.BASE_URL;
14+
if (import.meta.env.DEV) {
15+
return `${window.location.origin}/`;
16+
}
17+
if (base === '/' || base === '') {
18+
return `${window.location.origin}/`;
19+
}
20+
if (isIngressRelativeBase()) {
21+
try {
22+
const resolved = new URL(base, window.location.href).href;
23+
return resolved.endsWith('/') ? resolved : `${resolved}/`;
24+
} catch {
25+
return `${window.location.origin}/`;
26+
}
27+
}
28+
const path = base.endsWith('/') ? base : `${base}/`;
29+
return `${window.location.origin}${path}`;
30+
}
31+
32+
export function publicAssetUrl(relativePath: string): string {
33+
const rel = relativePath.replace(/^\//, '');
34+
return new URL(rel, getResolvedPublicBase()).href;
35+
}
36+
37+
/** Socket.IO `path` option: must include HA Ingress prefix when the UI is not at domain root. */
38+
export function getSocketIoPath(): string {
39+
if (import.meta.env.DEV) return '/socket.io';
40+
const base = import.meta.env.BASE_URL;
41+
if (base === '/' || base === '') return '/socket.io';
42+
if (isIngressRelativeBase()) {
43+
const p = window.location.pathname.replace(/\/+$/, '');
44+
return p ? `${p}/socket.io` : '/socket.io';
45+
}
46+
const normalized = base.endsWith('/') ? base.slice(0, -1) : base;
47+
return `${normalized}/socket.io`;
48+
}

client/vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import path from 'path'
66

77
function normalizeBase(raw: string): string {
88
if (!raw || raw === '/') return '/'
9+
// Home Assistant Ingress: relative base so built assets load under /hassio/ingress/<slug>/...
10+
if (raw === '.' || raw === './') return './'
911
return raw.endsWith('/') ? raw : `${raw}/`
1012
}
1113

0 commit comments

Comments
 (0)