Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fdb0039
feat: add basePath utility for sub-path deployment
alfonso46674 Jul 6, 2026
fea25c3
feat: prefix API_BASE_URL with the configured base path
alfonso46674 Jul 6, 2026
668283d
feat: prefix auth client base URL with the configured base path
alfonso46674 Jul 6, 2026
0eb0b94
fix: scope Task 3 auth-client fix to local jest mocks only
alfonso46674 Jul 6, 2026
3e4cdf6
feat: prefix i18n locale loadPath with the configured base path
alfonso46674 Jul 6, 2026
cb028d7
feat: configure React Router basename from the base path
alfonso46674 Jul 6, 2026
ac96514
feat: prefix hardcoded image src attributes with the base path
alfonso46674 Jul 6, 2026
2399a3e
feat: make Vite build output and index.html paths base-relative
alfonso46674 Jul 6, 2026
8219d75
fix: silence no-require-imports for jest.isolateModules pattern in ap…
alfonso46674 Jul 6, 2026
841508a
feat: substitute SPARKY_BASE_PATH into the served base href at runtime
alfonso46674 Jul 6, 2026
bc94510
docs: document SPARKY_BASE_PATH sub-path deployment
alfonso46674 Jul 6, 2026
db494e3
fix: correct nginx sub_filter pattern to match actual base href markup
alfonso46674 Jul 6, 2026
0463153
feat: prefix AI chat streaming endpoint with the configured base path
alfonso46674 Jul 6, 2026
348cacc
fix: route chat stream URL through the hooks/api layer instead of esl…
alfonso46674 Jul 6, 2026
e51478a
feat: use React Router Link for auth navigation to respect the base path
alfonso46674 Jul 6, 2026
429dd1a
feat: prefix exercise image and body-map SVG references with the base…
alfonso46674 Jul 6, 2026
f7d6402
feat: prefix developer resource links with the base path
alfonso46674 Jul 6, 2026
aa203f0
test: assert dist/index.html base href matches nginx's sub_filter pat…
alfonso46674 Jul 6, 2026
2d8f970
refactor: harden base-path config and reduce sub-path deployment churn
alfonso46674 Jul 7, 2026
a44cead
feat: add ESLint rule to catch hardcoded absolute paths bypassing SPA…
alfonso46674 Aug 9, 2026
60407ab
fix: prefix bump photo journal image src with the base path
alfonso46674 Aug 9, 2026
82a232e
fix: remove stale duplicate manifest and fix service worker offline f…
alfonso46674 Aug 10, 2026
5de0892
fix: wire SPARKY_BASE_PATH and BETTER_AUTH_URL through docker-compose…
alfonso46674 Aug 10, 2026
c1983da
docs: list SPARKY_BASE_PATH in the environment variables reference
alfonso46674 Aug 20, 2026
0330eaf
fix: address CodeRabbit review on the base-path ESLint rule
alfonso46674 Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Shared `no-restricted-syntax` selectors flagging a hardcoded,
// domain-root-absolute path used directly as a JSX `src=`/`href=` value or as
// a `fetch(...)` argument. Imported by both eslint.config.js (the rule) and
// src/tests/eslint/noHardcodedBasePath.test.ts (the test), so the two stay in
// sync automatically instead of drifting apart. Written as CommonJS (.cjs) so
// it loads cleanly from both eslint.config.js's native ESM `import` (Node
// synthesizes named exports from `exports.x = ...` via cjs-module-lexer) and
// Jest's CommonJS test runtime (plain `require()`, no transform needed).
exports.noHardcodedBasePathSelectors = [
{
selector:
'JSXAttribute[name.name=/^(src|href)$/] > Literal[value=/^\\/(?!\\/)/]',
message:
'Hardcoded absolute path bypasses SPARKY_BASE_PATH. Wrap it with withBasePath() from @/utils/basePath.',
},
{
selector:
'JSXAttribute[name.name=/^(src|href)$/] > JSXExpressionContainer > Literal[value=/^\\/(?!\\/)/]',
message:
'Hardcoded absolute path bypasses SPARKY_BASE_PATH. Wrap it with withBasePath() from @/utils/basePath.',
},
{
selector:
'JSXAttribute[name.name=/^(src|href)$/] > JSXExpressionContainer > TemplateLiteral > TemplateElement:first-child[value.raw=/^\\/(?!\\/)/]',
message:
'Hardcoded absolute path bypasses SPARKY_BASE_PATH. Wrap it with withBasePath() from @/utils/basePath.',
},
{
selector:
'CallExpression[callee.name="fetch"] > Literal[value=/^\\/(?!\\/)/]',
message:
'Hardcoded absolute path bypasses SPARKY_BASE_PATH. Wrap it with withBasePath() from @/utils/basePath.',
},
{
selector:
'CallExpression[callee.name="fetch"] > TemplateLiteral > TemplateElement:first-child[value.raw=/^\\/(?!\\/)/]',
message:
'Hardcoded absolute path bypasses SPARKY_BASE_PATH. Wrap it with withBasePath() from @/utils/basePath.',
},
{
selector:
'CallExpression[callee.type="MemberExpression"][callee.property.name="fetch"] > Literal[value=/^\\/(?!\\/)/]',
message:
'Hardcoded absolute path bypasses SPARKY_BASE_PATH. Wrap it with withBasePath() from @/utils/basePath.',
},
{
selector:
'CallExpression[callee.type="MemberExpression"][callee.property.name="fetch"] > TemplateLiteral > TemplateElement:first-child[value.raw=/^\\/(?!\\/)/]',
message:
'Hardcoded absolute path bypasses SPARKY_BASE_PATH. Wrap it with withBasePath() from @/utils/basePath.',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface NoHardcodedBasePathSelector {
selector: string;
message: string;
}

export const noHardcodedBasePathSelectors: NoHardcodedBasePathSelector[];
8 changes: 8 additions & 0 deletions SparkyFitnessFrontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import unusedImports from 'eslint-plugin-unused-imports';
import { noHardcodedBasePathSelectors } from './eslint-rules/noHardcodedBasePathSelectors.cjs';

export default tseslint.config(
{ ignores: ['dist', 'build', 'coverage', 'node_modules'] },
Expand Down Expand Up @@ -131,5 +132,12 @@ export default tseslint.config(
rules: {
'react-refresh/only-export-components': 'off',
},
},
{
files: ['src/**/*.{ts,tsx}'],
ignores: ['src/tests/**'],
rules: {
'no-restricted-syntax': ['error', ...noHardcodedBasePathSelectors],
},
}
);
10 changes: 5 additions & 5 deletions SparkyFitnessFrontend/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<base href="/" />
<meta charset="UTF-8" />
<meta
name="viewport"
Expand All @@ -10,20 +11,19 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="mobile-web-app-capable" content="yes" />
<title>SparkyFitness</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="/images/SparkyFitness.webp" />
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="images/SparkyFitness.webp" />
<meta name="description" content="Your Personal Fitness Companion" />
<meta name="author" content="Lovable" />
<link rel="manifest" href="/manifest.json" />

<meta property="og:title" content="SparkyFitness" />
<meta property="og:description" content="Your Personal Fitness Companion" />
<meta property="og:type" content="website" />
<meta property="og:image" content="/images/SparkyFitness.webp" />
<meta property="og:image" content="images/SparkyFitness.webp" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="" />
<meta name="twitter:image" content="/images/SparkyFitness.webp" />
<meta name="twitter:image" content="images/SparkyFitness.webp" />
</head>

<body>
Expand Down
21 changes: 0 additions & 21 deletions SparkyFitnessFrontend/public/manifest.json

This file was deleted.

9 changes: 7 additions & 2 deletions SparkyFitnessFrontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import { error as logError } from '@/utils/logging';
import { getUserLoggingLevel } from '@/utils/userPreferences.ts';
import { lazyWithChunkRecovery } from '@/utils/chunkRecovery';
import { getRouterBasename } from '@/utils/basePath';
const Auth = lazyWithChunkRecovery(() => import('@/pages/Auth/Auth'));
const ForgotPassword = lazyWithChunkRecovery(
() => import('@/pages/Auth/ForgotPassword')
Expand Down Expand Up @@ -300,7 +301,7 @@ const ReportsWrapper = () => {
return <Reports key={timezone} />;
};

const router = createBrowserRouter([
const routes = [
{
Component: Root,
ErrorBoundary: RootErrorBoundary,
Expand Down Expand Up @@ -454,7 +455,11 @@ const router = createBrowserRouter([
{ path: '*', Component: NotFound },
],
},
]);
];

const router = createBrowserRouter(routes, {
basename: getRouterBasename(),
});

const App = () => {
return (
Expand Down
4 changes: 3 additions & 1 deletion SparkyFitnessFrontend/src/api/Chatbot/sparkyChatService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { apiCall } from '@/api/api';
import { apiCall, API_BASE_URL } from '@/api/api';
import { error } from '@/utils/logging';
import { Message } from '@/types/Chatbot_types';

Expand All @@ -12,6 +12,8 @@ interface ChatHistory extends Message {
created_at: string;
}

export const getChatStreamUrl = (): string => `${API_BASE_URL}/chat/stream`;

export const loadUserPreferences = async (): Promise<UserPreferences> => {
const data = await apiCall(`/user-preferences`, {
method: 'GET',
Expand Down
3 changes: 2 additions & 1 deletion SparkyFitnessFrontend/src/api/Exercises/exerciseService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { apiCall } from '@/api/api';
import { withBasePath } from '@/utils/basePath';
import { ExerciseCSVData } from '@/pages/Exercises/ExerciseImportCSV';
import {
Exercise,
Expand Down Expand Up @@ -280,7 +281,7 @@ export const importFitFiles = async (
};

export const getBodyMapSvg = async (): Promise<string> => {
const response = await fetch('/images/muscle-male.svg');
const response = await fetch(withBasePath('/images/muscle-male.svg'));
if (!response.ok) {
throw new Error('Failed to fetch SVG');
}
Expand Down
3 changes: 2 additions & 1 deletion SparkyFitnessFrontend/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { toast } from '@/hooks/use-toast';
import * as logging from '@/utils/logging';
import { getUserLoggingLevel } from '@/utils/userPreferences';
import { getBasePath } from '@/utils/basePath';

interface ApiCallOptions extends RequestInit {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -15,7 +16,7 @@ interface ApiCallOptions extends RequestInit {

class HttpApiError extends Error {}

export const API_BASE_URL = '/api';
export const API_BASE_URL = `${getBasePath()}/api`;
//export const API_BASE_URL = 'http://192.168.1.111:3010';

// A single-use guard so a reload triggered by gateway interception (see
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useChatbotVisibility } from '@/contexts/ChatbotVisibilityContext';
import { useIsMobile } from '@/hooks/use-mobile';
import { useAuth } from '@/hooks/useAuth';
import { useActiveAIService } from '@/hooks/AI/useAIServiceSettings';
import { withBasePath } from '@/utils/basePath';

const BUTTON_SIZE = 56; // 14 * 4 = 56px (w-14)
const MINIMIZED_SIZE = 24;
Expand Down Expand Up @@ -378,7 +379,7 @@ const DraggableChatbotButton: React.FC = () => {
)}

<img
src="/images/chatbot.gif"
src={withBasePath('/images/chatbot.gif')}
alt="AI Chatbot"
className={`w-full h-full object-contain drop-shadow-lg pointer-events-none
${isDragging ? 'scale-110' : 'hover:scale-110'} transition-transform duration-200`}
Expand Down
4 changes: 4 additions & 0 deletions SparkyFitnessFrontend/src/hooks/AI/useSparkyChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
} from '@/api/Chatbot/sparkyChatService';
import { chatbotKeys } from '@/api/keys/ai';

// Re-exported so consumers reach the chat-stream URL through the hooks layer,
// keeping the api/ layer boundary consistent with the rest of the chat surface.
export { getChatStreamUrl } from '@/api/Chatbot/sparkyChatService';

export const useChatPreferencesQuery = () => {
const { t } = useTranslation();

Expand Down
7 changes: 6 additions & 1 deletion SparkyFitnessFrontend/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpApi from 'i18next-http-backend';
import { getSupportedLanguages } from './utils/languageUtils';
import { withBasePath } from './utils/basePath';

export function getLocalesLoadPath(): string {
return withBasePath('/locales/{{lng}}/{{ns}}.json');
}

i18n
.use(HttpApi)
Expand All @@ -23,7 +28,7 @@ i18n
caches: ['localStorage', 'cookie'],
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
loadPath: getLocalesLoadPath(),
},
react: {
useSuspense: false,
Expand Down
3 changes: 2 additions & 1 deletion SparkyFitnessFrontend/src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { useCurrentVersionQuery } from '@/hooks/useGeneralQueries';
import { useCycleSettings } from '@/hooks/useCycle';
import { cn } from '@/lib/utils';
import { getGridClassNormal } from '@/utils/layout';
import { withBasePath } from '@/utils/basePath';

interface AddCompItem {
value: string;
Expand Down Expand Up @@ -439,7 +440,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({
<div className="flex justify-between items-center mb-6">
<div className="flex items-center gap-1">
<img
src="/images/SparkyFitness.webp"
src={withBasePath('/images/SparkyFitness.webp')}
alt="SparkyFitness Logo"
width={54}
height={72}
Expand Down
9 changes: 7 additions & 2 deletions SparkyFitnessFrontend/src/lib/auth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import { apiKeyClient } from '@better-auth/api-key/client';
import { ssoClient } from '@better-auth/sso/client';
import { passkeyClient } from '@better-auth/passkey/client';
import { BetterAuthClientPlugin } from 'better-auth';
import { getBasePath } from '@/utils/basePath';

export function getAuthBaseUrl(): string {
return window.location.origin + getBasePath() + '/api/auth';
}

export const authClient = createAuthClient({
// Use /api/auth as the base URL.
baseURL: window.location.origin + '/api/auth',
// Use /api/auth as the base URL, adjusted for the configured sub-path.
baseURL: getAuthBaseUrl(),
plugins: [
magicLinkClient(),
adminClient() as unknown as BetterAuthClientPlugin,
Expand Down
11 changes: 6 additions & 5 deletions SparkyFitnessFrontend/src/pages/Auth/Auth.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Expand Down Expand Up @@ -35,6 +35,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { AuthResponse } from '@/types/auth';
import { getErrorMessage } from '@/utils/api';
import { useTranslation } from 'react-i18next';
import { withBasePath } from '@/utils/basePath';

const Auth = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -415,7 +416,7 @@ const Auth = () => {
<CardHeader className="text-center">
<div className="flex items-center justify-center mb-4">
<img
src="/images/SparkyFitness.webp"
src={withBasePath('/images/SparkyFitness.webp')}
alt="SparkyFitness Logo"
className="h-10 w-10 mr-2"
/>
Expand Down Expand Up @@ -514,12 +515,12 @@ const Auth = () => {
/>
</div>
<div className="text-right text-sm">
<a
href="/forgot-password"
<Link
to="/forgot-password"
className="font-medium text-primary hover:underline"
>
Forgot password?
</a>
</Link>
</div>
<Button
type="submit"
Expand Down
8 changes: 5 additions & 3 deletions SparkyFitnessFrontend/src/pages/Auth/ForgotPassword.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Expand All @@ -13,6 +14,7 @@ import { usePreferences } from '@/contexts/PreferencesContext';
import { debug, info } from '@/utils/logging';
import { useRequestPasswordResetMutation } from '@/hooks/Auth/useAuth';
import { getErrorMessage } from '@/utils/api';
import { withBasePath } from '@/utils/basePath';

const ForgotPassword = () => {
const { loggingLevel } = usePreferences();
Expand Down Expand Up @@ -49,7 +51,7 @@ const ForgotPassword = () => {
<CardHeader className="text-center">
<div className="flex items-center justify-center mb-4">
<img
src="/images/SparkyFitness.webp"
src={withBasePath('/images/SparkyFitness.webp')}
alt="SparkyFitness Logo"
className="h-10 w-10 mr-2"
/>
Expand Down Expand Up @@ -84,9 +86,9 @@ const ForgotPassword = () => {
</p>
)}
<div className="text-center text-sm">
<a href="/" className="font-medium text-primary hover:underline">
<Link to="/" className="font-medium text-primary hover:underline">
Back to Sign In
</a>
</Link>
</div>
</form>
</CardContent>
Expand Down
Loading
Loading