diff --git a/backend/app/services/status_checker.py b/backend/app/services/status_checker.py index c654183..b682704 100644 --- a/backend/app/services/status_checker.py +++ b/backend/app/services/status_checker.py @@ -145,7 +145,9 @@ def _parse_override(raw: str) -> tuple[str, str | None, int | None]: a service may carry its own host, optionally with a scheme and a port (`blog.example.com`, `blog.example.com:8443`, `https://blog.example.com`). """ - rest = raw + # Like a node ip, an override may list several hosts — the first one wins, + # same as the frontend's `splitFirstHost`. + rest = raw.split(",")[0].strip() scheme: str | None = None for prefix in ("https://", "http://"): if rest.lower().startswith(prefix): diff --git a/backend/tests/test_status_checker.py b/backend/tests/test_status_checker.py index a5437ab..11b8b5a 100644 --- a/backend/tests/test_status_checker.py +++ b/backend/tests/test_status_checker.py @@ -527,6 +527,12 @@ async def test_check_service_lets_the_service_port_beat_the_override_port(): assert await _captured_url(svc, "10.0.0.1") == "http://blog.example.com:3000" +@pytest.mark.asyncio +async def test_check_service_uses_the_first_host_of_a_comma_list_override(): + svc = {"port": 8080, "protocol": "tcp", "service_name": "blog", "host": "blog.example.com, alt.example.com"} + assert await _captured_url(svc, "10.0.0.1") == "http://blog.example.com:8080" + + @pytest.mark.asyncio async def test_check_service_brackets_an_ipv6_override(): svc = {"port": 80, "protocol": "tcp", "service_name": "http", "host": "[2001:db8::1]:8080"} diff --git a/frontend/src/hooks/useStatusPolling.ts b/frontend/src/hooks/useStatusPolling.ts index 7b59c39..e6ccff4 100644 --- a/frontend/src/hooks/useStatusPolling.ts +++ b/frontend/src/hooks/useStatusPolling.ts @@ -6,6 +6,9 @@ import type { ServiceStatus } from '@/types' interface ServiceStatusEntry { port?: number protocol?: string + /** Per-service host override, part of the overlay key — several vhosts can + * share one port on one node. Null when the service has none. */ + host?: string | null status: ServiceStatus } diff --git a/frontend/src/stores/__tests__/canvasStore/nodes.test.ts b/frontend/src/stores/__tests__/canvasStore/nodes.test.ts index 5b95636..f7df2b6 100644 --- a/frontend/src/stores/__tests__/canvasStore/nodes.test.ts +++ b/frontend/src/stores/__tests__/canvasStore/nodes.test.ts @@ -49,6 +49,12 @@ describe('canvasStore — nodes', () => { expect(serviceStatuses['node-1:443/tcp@shop.example.com']).toBe('offline') }) + it('setServiceStatuses treats a null host from the wire as no override', () => { + const { setServiceStatuses } = useCanvasStore.getState() + setServiceStatuses('node-1', [{ port: 80, protocol: 'tcp', host: null, status: 'online' }]) + expect(useCanvasStore.getState().serviceStatuses['node-1:80/tcp@']).toBe('online') + }) + it('setServiceStatuses merges without dropping other nodes', () => { const { setServiceStatuses } = useCanvasStore.getState() setServiceStatuses('node-1', [{ port: 80, protocol: 'tcp', status: 'online' }]) diff --git a/frontend/src/stores/canvasStore.ts b/frontend/src/stores/canvasStore.ts index 6359673..0b6aa80 100644 --- a/frontend/src/stores/canvasStore.ts +++ b/frontend/src/stores/canvasStore.ts @@ -104,7 +104,7 @@ function translateWaypointsForMovedNodes( /** Key for the live per-service status overlay. * `host` is part of the key because several vhosts can share one port on one * node — without it they'd all read the same status. */ -export const serviceStatusKey = (nodeId: string, port?: number, protocol?: string, host?: string): string => +export const serviceStatusKey = (nodeId: string, port?: number, protocol?: string, host?: string | null): string => `${nodeId}:${port ?? ''}/${protocol ?? ''}@${host?.trim() ?? ''}` interface CanvasState { @@ -188,7 +188,7 @@ interface CanvasState { fitViewPending: boolean clearFitViewPending: () => void notifyScanDeviceFound: () => void - setServiceStatuses: (nodeId: string, statuses: { port?: number; protocol?: string; host?: string; status: ServiceStatus }[]) => void + setServiceStatuses: (nodeId: string, statuses: { port?: number; protocol?: string; host?: string | null; status: ServiceStatus }[]) => void hideIp: boolean toggleHideIp: () => void setHideIp: (value: boolean) => void