dashboard: honor deploy-time API base for live metrics stream (thanks @porkmagus) #96

Merged
jcoffey-dev merged 3 commits from fix/live-metrics-api-base into main 2026-10-09 07:23:09 +00:00
2 changed files with 64 additions and 4 deletions

No files matched your search

@@ -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('<meta name="api-base-url" content="https://mail.example.org" />');
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);
});
});
@@ -2,11 +2,13 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* 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<MetricId>) {
}
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<typeof setTimeout> | null = null;