diff --git a/public/views/service_url.js b/public/views/service_url.js new file mode 100644 index 0000000..add9b4a --- /dev/null +++ b/public/views/service_url.js @@ -0,0 +1,28 @@ +// Pure helpers: choose which URL a service tile opens, based on whether the dashboard +// is being viewed on-network (LAN) or remotely. No DOM / no network access here. + +const PRIVATE_RE = /^(localhost$|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/; + +// True when `hostname` is NOT a private/local address — i.e. the dashboard is being +// accessed remotely (e.g. via void.hynesy.com). +export function isRemoteHost(hostname = '') { + const h = String(hostname).toLowerCase(); + if (!h) return false; + if (PRIVATE_RE.test(h)) return false; + if (h.endsWith('.local') || h.endsWith('.lan')) return false; + if (!h.includes('.')) return false; // bare hostname (e.g. "ct311") + return true; +} + +// For a service ({ url, external }) and whether we're remote, return the primary URL +// the tile opens, an optional alt (the other URL) for one-click fallback, and whether +// the service is LAN-only while remote. +export function pickServiceUrls(svc = {}, remote = false) { + const url = svc.url || ''; + const external = svc.external || ''; + let primary, alt; + if (remote) { primary = external || url; alt = external ? url : ''; } + else { primary = url; alt = external || ''; } + if (alt && alt === primary) alt = ''; + return { primary, alt: alt || null, lanOnly: remote && !external }; +} diff --git a/tests/frontend/service_url.test.js b/tests/frontend/service_url.test.js new file mode 100644 index 0000000..251d9d8 --- /dev/null +++ b/tests/frontend/service_url.test.js @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { isRemoteHost, pickServiceUrls } from '../../public/views/service_url.js'; + +describe('isRemoteHost', () => { + it('treats private/local hosts as not-remote', () => { + for (const h of ['localhost', '127.0.0.1', '192.168.1.216', '10.0.0.5', '172.16.0.1', 'void.local', 'ct311', 'nas.lan']) + expect(isRemoteHost(h)).toBe(false); + }); + it('treats public domains as remote', () => { + for (const h of ['void.hynesy.com', 'dash.example.org']) + expect(isRemoteHost(h)).toBe(true); + }); +}); + +describe('pickServiceUrls', () => { + const svc = { url: 'http://192.168.1.99', external: 'https://gramps.hynesy.com' }; + it('remote + external → domain primary, LAN alt', () => { + expect(pickServiceUrls(svc, true)).toEqual({ primary: 'https://gramps.hynesy.com', alt: 'http://192.168.1.99', lanOnly: false }); + }); + it('local + external → LAN primary, domain alt', () => { + expect(pickServiceUrls(svc, false)).toEqual({ primary: 'http://192.168.1.99', alt: 'https://gramps.hynesy.com', lanOnly: false }); + }); + it('remote + no external → LAN primary, no alt, lanOnly', () => { + expect(pickServiceUrls({ url: 'http://192.168.1.5' }, true)).toEqual({ primary: 'http://192.168.1.5', alt: null, lanOnly: true }); + }); + it('local + no external → LAN primary, no alt', () => { + expect(pickServiceUrls({ url: 'http://192.168.1.5' }, false)).toEqual({ primary: 'http://192.168.1.5', alt: null, lanOnly: false }); + }); +});