diff --git a/src/features/dashboard/stores/liveMetricsStore.test.ts b/src/features/dashboard/stores/liveMetricsStore.test.ts new file mode 100644 index 0000000..f447b65 --- /dev/null +++ b/src/features/dashboard/stores/liveMetricsStore.test.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, it, expect, afterEach, vi } from 'vitest'; + +const originalHead = document.head.innerHTML; +const seenUrls: string[] = []; + +class FakeEventSource { + url: string; + constructor(url: string) { + this.url = url; + seenUrls.push(url); + } + addEventListener() {} + close() {} +} + +async function subscribeWithMeta(meta: string) { + document.head.innerHTML = meta; + vi.resetModules(); + vi.stubGlobal('EventSource', FakeEventSource); + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response('tok123', { status: 200 })), + ); + const { useLiveMetricsStore } = await import('./liveMetricsStore'); + useLiveMetricsStore.getState().subscribe(new Set(['cpu' as never])); + await new Promise((r) => setTimeout(r, 300)); + useLiveMetricsStore.getState().unsubscribe(); + return seenUrls.at(-1) ?? ''; +} + +afterEach(() => { + document.head.innerHTML = originalHead; + seenUrls.length = 0; + vi.resetModules(); + vi.unstubAllGlobals(); +}); + +describe('live metrics stream URL', () => { + it('honors the deploy-time api-base-url instead of the Admin origin', async () => { + const url = await subscribeWithMeta(''); + expect(url.startsWith('https://mail.example.org/api/live/metrics?')).toBe(true); + }); + + it('falls back to the Admin origin when no api-base-url is injected', async () => { + const url = await subscribeWithMeta(''); + expect(url.startsWith(`${window.location.origin}/api/live/metrics?`)).toBe(true); + }); +}); diff --git a/src/features/dashboard/stores/liveMetricsStore.ts b/src/features/dashboard/stores/liveMetricsStore.ts index 54396bb..becaf57 100644 --- a/src/features/dashboard/stores/liveMetricsStore.ts +++ b/src/features/dashboard/stores/liveMetricsStore.ts @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. */ import { create } from 'zustand'; import type { MetricId, Metric } from '../types/metrics'; -import { apiFetch } from '@/services/api'; +import { apiFetch, getApiBaseUrl } from '@/services/api'; import i18n from '@/i18n'; type LiveStatus = 'idle' | 'connecting' | 'open' | 'error' | 'closed'; @@ -107,9 +109,13 @@ async function openStream(ids: Set) { } function getOrigin(): string { - const envUrl = import.meta.env.VITE_API_BASE_URL as string | undefined; - if (envUrl && envUrl.length > 0) return envUrl.replace(/\/+$/, ''); - return window.location.origin; + // Honor the deploy-time API base (VITE_API_BASE_URL or the api-base-url + // meta tag) exactly like every other /api/live caller. Using + // window.location.origin here breaks Dashboard live metrics in any split + // deployment where the Admin origin is not the server (e.g. Admin in Docker + // pointed at the server via API_BASE_URL): the EventSource hits the Admin + // nginx instead, which answers with the SPA shell. + return getApiBaseUrl(); } let visibilityTimer: ReturnType | null = null;