From a7dba3bbcc3f55736077cbc4d062bfb174cdfb08 Mon Sep 17 00:00:00 2001 From: nguyenha935 Date: Sun, 19 Jul 2026 01:14:18 +0700 Subject: [PATCH] fix(dashboard): prefer public endpoint URLs (#7547) * fix(dashboard): prefer public endpoint URLs * docs: add changelog fragment for #7547 * test(dashboard): cover onboarding public endpoint * refactor(hooks): split display-URL predicates below complexity gate Decompose isPrivateIpv4 and isPublicDisplayBaseUrl (both over the ESLint complexity gate of 15) into small named predicates. Behavior is unchanged: - isPrivateIpv4 now checks a PRIVATE_IPV4_RANGES table (RFC1918 + special-use ranges) through isInIpv4Range instead of one long chain of ||/&& comparisons. - isPublicDisplayBaseUrl now delegates to isSupportedProtocol, isLoopbackHostname, isMulticastDnsHostname and isNonPublicIpv6 (itself split into isIpv6LoopbackOrUnspecified / isIpv6UniqueLocal / isIpv6LinkLocal), preserving the isIpv6 gate so hostnames that merely start with "fc"/"fd" (e.g. fdroid.example.com) are not misclassified as IPv6 unique-local addresses. Adds IPv4 range-boundary and IPv6-gate regression tests; all existing assertions are unchanged. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../fixes/7547-prefer-public-endpoint-url.md | 1 + .../dashboard/endpoint/EndpointPageClient.tsx | 56 ++++---- .../__tests__/ApiEndpointsTab.test.tsx | 16 ++- .../__tests__/EndpointPageClient.test.tsx | 52 ++++++++ .../(dashboard)/dashboard/onboarding/page.tsx | 11 +- src/app/api/network/info/route.ts | 3 +- .../__tests__/useDisplayBaseUrl.test.tsx | 108 +++++++++++++++- src/shared/hooks/index.ts | 7 +- src/shared/hooks/useDisplayBaseUrl.ts | 122 +++++++++++++++++- .../ui/onboarding-public-endpoint.test.tsx | 107 +++++++++++++++ 10 files changed, 429 insertions(+), 54 deletions(-) create mode 100644 changelog.d/fixes/7547-prefer-public-endpoint-url.md create mode 100644 tests/unit/ui/onboarding-public-endpoint.test.tsx diff --git a/changelog.d/fixes/7547-prefer-public-endpoint-url.md b/changelog.d/fixes/7547-prefer-public-endpoint-url.md new file mode 100644 index 0000000000..387f0d09a1 --- /dev/null +++ b/changelog.d/fixes/7547-prefer-public-endpoint-url.md @@ -0,0 +1 @@ +- **fix(dashboard):** Public and managed tunnel endpoints now take precedence over loopback URLs in dashboard setup and copyable API configuration ([#7547](https://github.com/diegosouzapw/OmniRoute/pull/7547)) — thanks @nguyenha935 diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx index 54e27f4991..f2972f1ccc 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { Card, Button, Input, Modal, CardSkeleton, SegmentedControl } from "@/shared/components"; import Toggle from "@/shared/components/Toggle"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; -import { useDisplayBaseUrl } from "@/shared/hooks"; +import { isPublicDisplayBaseUrl, useDisplayBaseUrl } from "@/shared/hooks"; import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers"; import { getProviderDisplayName } from "@/lib/display/names"; import { useTranslations } from "next-intl"; @@ -19,12 +19,7 @@ const CLOUD_ACTION_TIMEOUT_MS = 15000; type TranslationValues = Record; type CloudflaredTunnelPhase = - | "unsupported" - | "not_installed" - | "stopped" - | "starting" - | "running" - | "error"; + "unsupported" | "not_installed" | "stopped" | "starting" | "running" | "error"; type CloudflaredTunnelStatus = { supported: boolean; @@ -43,12 +38,7 @@ type CloudflaredTunnelStatus = { }; type TailscaleTunnelPhase = - | "unsupported" - | "not_installed" - | "needs_login" - | "stopped" - | "running" - | "error"; + "unsupported" | "not_installed" | "needs_login" | "stopped" | "running" | "error"; type TailscaleTunnelStatus = { supported: boolean; @@ -70,13 +60,7 @@ type TailscaleTunnelStatus = { }; type NgrokTunnelPhase = - | "unsupported" - | "not_installed" - | "stopped" - | "needs_auth" - | "starting" - | "running" - | "error"; + "unsupported" | "not_installed" | "stopped" | "needs_auth" | "starting" | "running" | "error"; type NgrokTunnelStatus = { supported: boolean; @@ -187,6 +171,7 @@ export default function APIPageClient({ machineId }: Readonly(null); + const [localApiUrl, setLocalApiUrl] = useState("http://localhost:20128/v1"); const [lanUrls, setLanUrls] = useState([]); const [tailscaleIpUrl, setTailscaleIpUrl] = useState(null); const [activeEndpointTab, setActiveEndpointTab] = useState("apis"); @@ -335,6 +320,7 @@ export default function APIPageClient({ machineId }: Readonly + candidates.findIndex((other) => other.url === candidate.url) === index + ); const visibleTunnelCount = [showCloudflaredTunnel, showTailscaleFunnel, showNgrokTunnel].filter( Boolean ).length; @@ -1357,7 +1355,7 @@ export default function APIPageClient({ machineId }: Readonly