Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 3 additions & 12 deletions frontend/taskdeck-web/src/api/versionApi.ts
Original file line number Diff line number Diff line change
@@ -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).
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions frontend/taskdeck-web/src/composables/useBoardRealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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`
}

Expand Down
27 changes: 27 additions & 0 deletions frontend/taskdeck-web/src/tests/utils/apiRoot.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
7 changes: 7 additions & 0 deletions frontend/taskdeck-web/src/utils/apiRoot.ts
Original file line number Diff line number Diff line change
@@ -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, '')
}
Loading