From 032bbbbad57b43d7bde3b61a4a0abf16774ace55 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 6 Sep 2026 05:24:37 +0100 Subject: [PATCH] Unify frontend API root normalization --- frontend/taskdeck-web/src/api/versionApi.ts | 15 +++-------- .../src/composables/useBoardRealtime.ts | 5 ++-- .../src/tests/utils/apiRoot.spec.ts | 27 +++++++++++++++++++ frontend/taskdeck-web/src/utils/apiRoot.ts | 7 +++++ 4 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 frontend/taskdeck-web/src/tests/utils/apiRoot.spec.ts create mode 100644 frontend/taskdeck-web/src/utils/apiRoot.ts diff --git a/frontend/taskdeck-web/src/api/versionApi.ts b/frontend/taskdeck-web/src/api/versionApi.ts index cfaaf5ea0..3e7f29fe8 100644 --- a/frontend/taskdeck-web/src/api/versionApi.ts +++ b/frontend/taskdeck-web/src/api/versionApi.ts @@ -1,5 +1,8 @@ import http from './http' import type { RetryableRequestConfig } from './httpRetry' +import { apiRootFrom } from '../utils/apiRoot' + +export { apiRootFrom } from '../utils/apiRoot' /** * Product-version lookup (#1948). @@ -27,18 +30,6 @@ export interface LiveHealthResponse { timestamp?: string } -/** - * Pure derivation of the server root from an API base: strips one trailing - * `/api` segment, with or without its trailing slash. Split out from - * `resolveApiRoot()` because `VITE_API_BASE_URL` is inlined at build time — - * a test can only reach the *rule* through a function that takes the base as - * an argument, and every deployment shape (`/api`, `/taskdeck/api`, an - * absolute origin, empty) has to be covered. - */ -export function apiRootFrom(apiBase: string): string { - return apiBase.replace(/\/api\/?$/i, '') -} - /** * Server root for endpoints that sit outside the `/api` prefix. Mirrors * `useBoardRealtime.resolveHubUrl()`; returns `''` for the packaged deployment diff --git a/frontend/taskdeck-web/src/composables/useBoardRealtime.ts b/frontend/taskdeck-web/src/composables/useBoardRealtime.ts index ab25efec0..05271e4c6 100644 --- a/frontend/taskdeck-web/src/composables/useBoardRealtime.ts +++ b/frontend/taskdeck-web/src/composables/useBoardRealtime.ts @@ -8,6 +8,7 @@ import { import type { BoardPresenceSnapshot, BoardRealtimeEvent } from '../types/realtime' import { getToken } from '../utils/tokenStorage' import { logWarn } from '../utils/errorReporting' +import { apiRootFrom } from '../utils/apiRoot' const BOARD_MUTATION_EVENT = 'boardMutation' const BOARD_PRESENCE_EVENT = 'boardPresence' @@ -19,9 +20,9 @@ const FALLBACK_POLL_INTERVAL_MS = 30000 // prevents the ~3 req/s thrash observed with rapid SignalR event bursts. const MUTATION_DEBOUNCE_MS = 300 -function resolveHubUrl(): string { +export function resolveHubUrl(): string { const apiBase = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api' - const apiRoot = apiBase.replace(/\/api\/?$/i, '') + const apiRoot = apiRootFrom(apiBase) return `${apiRoot}/hubs/boards` } diff --git a/frontend/taskdeck-web/src/tests/utils/apiRoot.spec.ts b/frontend/taskdeck-web/src/tests/utils/apiRoot.spec.ts new file mode 100644 index 000000000..5f02941fd --- /dev/null +++ b/frontend/taskdeck-web/src/tests/utils/apiRoot.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { resolveHubUrl } from '../../composables/useBoardRealtime' +import { apiRootFrom } from '../../utils/apiRoot' + +describe('apiRootFrom', () => { + it.each([ + ['', ''], + ['/', '/'], + ['/api', ''], + ['/api/', ''], + ['/API/', ''], + ['http://localhost:5000/api', 'http://localhost:5000'], + ['https://example.test/taskdeck/api/', 'https://example.test/taskdeck'], + ['/taskdeck/api', '/taskdeck'], + ['/taskdeck/api/', '/taskdeck'], + ['/taskdeck/apiary', '/taskdeck/apiary'], + ['/taskdeck/api/cards', '/taskdeck/api/cards'], + ['https://example.test/api/api', 'https://example.test/api'], + ['api', 'api'], + ])('normalizes %o to %o', (apiBase, expected) => { + expect(apiRootFrom(apiBase)).toBe(expected) + }) + + it('keeps the realtime hub suffix outside the shared root utility', () => { + expect(resolveHubUrl()).toBe('http://localhost:5000/hubs/boards') + }) +}) diff --git a/frontend/taskdeck-web/src/utils/apiRoot.ts b/frontend/taskdeck-web/src/utils/apiRoot.ts new file mode 100644 index 000000000..f500f3f5a --- /dev/null +++ b/frontend/taskdeck-web/src/utils/apiRoot.ts @@ -0,0 +1,7 @@ +/** + * Remove exactly one terminal `/api` segment while preserving deployment + * subpaths, origins, and unrelated path segments. + */ +export function apiRootFrom(apiBase: string): string { + return apiBase.replace(/\/api\/?$/i, '') +}