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;