FN-006: label Cloudflare tunnel URLs accurately
Label remote access URLs with terminology that matches the active tunnel provider. - derive share URL labels from the running remote provider - add Cloudflare and neutral tunnel label translations and generated i18n types - cover Cloudflare, Tailscale, and unknown-provider URL labels - add a patch changeset for the corrected remote-access terminology Files changed: .changeset/fn-006-remote-url-label.md | 7 ++++++ .../SettingsModal.remote-notifications.test.tsx | 28 ++++++++++++++++++++-- .../components/settings/sections/RemoteSection.tsx | 21 +++++++++++----- packages/i18n/locales/en/app.json | 2 ++ packages/i18n/locales/es/app.json | 4 +++- packages/i18n/locales/fr/app.json | 4 +++- packages/i18n/locales/ko/app.json | 4 +++- packages/i18n/locales/pt-BR/app.json | 4 +++- packages/i18n/locales/zh-CN/app.json | 4 +++- packages/i18n/locales/zh-TW/app.json | 4 +++- packages/i18n/src/resources.d.ts | 21 +++++++++++++--- 11 files changed, 86 insertions(+), 17 deletions(-) Fusion-Task-Id: FN-006 Fusion-Task-Lineage: a34a3c77-11ab-4f73-a264-9776e75edfae Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-006-remote-url-label.md
Normal file
7
.changeset/fn-006-remote-url-label.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Remote Access now labels a Cloudflare tunnel URL correctly instead of calling it a Tailnet URL.
|
||||
category: fix
|
||||
dev: RemoteSection derives the share-block label from remoteStatus.provider; adds settings.remote.cloudflareTunnelURL and settings.remote.tunnelURL.
|
||||
@@ -517,14 +517,38 @@ describe("SettingsModal", () => {
|
||||
expect(document.querySelector(".remote-share-block")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders remote-share-block when tunnel is running with a URL", async () => {
|
||||
it("labels a running Cloudflare tunnel URL with Cloudflare terminology", async () => {
|
||||
mockFetchRemoteStatus.mockResolvedValue({ provider: "cloudflare", state: "running", url: "https://demo-tunnel.trycloudflare.com/", lastError: null });
|
||||
await renderModalSection("remote", "Remote Access");
|
||||
|
||||
const shareBlock = document.querySelector(".remote-share-block");
|
||||
expect(shareBlock).toBeInTheDocument();
|
||||
expect(shareBlock?.textContent).toContain("Cloudflare tunnel URL:");
|
||||
expect(shareBlock?.textContent).toContain("https://demo-tunnel.trycloudflare.com/");
|
||||
expect(shareBlock?.textContent).not.toContain("Tailnet");
|
||||
});
|
||||
|
||||
it("retains the Tailnet URL label for a running Tailscale tunnel", async () => {
|
||||
mockFetchRemoteStatus.mockResolvedValue({ provider: "tailscale", state: "running", url: "https://machine.ts.net/", lastError: null });
|
||||
await renderModalSection("remote", "Remote Access");
|
||||
|
||||
const statusBar = document.querySelector(".remote-status-bar");
|
||||
const shareBlock = document.querySelector(".remote-share-block");
|
||||
expect(statusBar).toBeInTheDocument();
|
||||
expect(statusBar?.className).toContain("remote-status-bar--running");
|
||||
expect(document.querySelector(".remote-share-block")).toBeInTheDocument();
|
||||
expect(shareBlock?.textContent).toContain("Tailnet URL:");
|
||||
expect(shareBlock?.textContent).toContain("https://machine.ts.net/");
|
||||
});
|
||||
|
||||
it("uses a neutral URL label when the running tunnel provider is unknown", async () => {
|
||||
mockFetchRemoteStatus.mockResolvedValue({ provider: null, state: "running", url: "https://unknown.example/", lastError: null });
|
||||
await renderModalSection("remote", "Remote Access");
|
||||
|
||||
const shareBlock = document.querySelector(".remote-share-block");
|
||||
expect(shareBlock).toBeInTheDocument();
|
||||
expect(shareBlock?.textContent).toContain("Tunnel URL:");
|
||||
expect(shareBlock?.textContent).toContain("https://unknown.example/");
|
||||
expect(shareBlock?.textContent).not.toContain("Tailnet");
|
||||
});
|
||||
|
||||
it("updates provider selection via radio and shows provider status", async () => {
|
||||
|
||||
@@ -122,22 +122,31 @@ export function RemoteSection({ form, setForm, remote }: RemoteSectionProps) {
|
||||
</div>)}
|
||||
{tunnelState === "running" && (remoteStatus?.url || tunnelShareLink) && (() => {
|
||||
let accessCode: string | null = null;
|
||||
let tailnetUrl: string | null = remoteStatus?.url ?? null;
|
||||
let shareUrl: string | null = remoteStatus?.url ?? null;
|
||||
if (tunnelShareLink?.url) {
|
||||
try {
|
||||
const parsed = new URL(tunnelShareLink.url);
|
||||
accessCode = parsed.searchParams.get("rt");
|
||||
if (!tailnetUrl)
|
||||
tailnetUrl = `${parsed.origin}/`;
|
||||
if (!shareUrl)
|
||||
shareUrl = `${parsed.origin}/`;
|
||||
}
|
||||
catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
/*
|
||||
FNXC:RemoteAccess 2026-08-18-07:10:
|
||||
The share-block URL label must name the provider that is actually running (`remoteStatus.provider`), not the radio selection in `form` — the radio can hold an unsaved provider while a tunnel from the other provider is still up. A Cloudflare tunnel URL was previously labelled "Tailnet URL:", applying Tailscale vocabulary to a trycloudflare.com address. Unknown/null provider falls back to neutral "Tunnel URL:" so the row never asserts a provider it cannot prove.
|
||||
*/
|
||||
const shareUrlLabel = remoteStatus?.provider === "cloudflare"
|
||||
? t("settings.remote.cloudflareTunnelURL", "Cloudflare tunnel URL:")
|
||||
: remoteStatus?.provider === "tailscale"
|
||||
? t("settings.remote.tailnetURL", "Tailnet URL:")
|
||||
: t("settings.remote.tunnelURL", "Tunnel URL:");
|
||||
return (<div className="remote-share-block">
|
||||
{tailnetUrl && (<div className="remote-share-row">
|
||||
<small>{t("settings.remote.tailnetURL", "Tailnet URL:")}</small>
|
||||
<code className="settings-url-output">{tailnetUrl}</code>
|
||||
{shareUrl && (<div className="remote-share-row">
|
||||
<small>{shareUrlLabel}</small>
|
||||
<code className="settings-url-output">{shareUrl}</code>
|
||||
</div>)}
|
||||
{accessCode && (<div className="remote-share-row">
|
||||
<small>{t("settings.remote.remoteAccessCode", "Remote access code:")}</small>
|
||||
|
||||
@@ -6656,6 +6656,7 @@
|
||||
"authLinkTokenType": "Auth link token type",
|
||||
"automaticallyRestoreTunnelOnStartupIfItWas": "Automatically restore tunnel on startup if it was running when last stopped. Default: disabled.",
|
||||
"cloudflare": "Cloudflare",
|
||||
"cloudflareTunnelURL": "Cloudflare tunnel URL:",
|
||||
"cloudflaredInstalled": "cloudflared installed successfully",
|
||||
"cloudflaredIsInstalled": "cloudflared is installed",
|
||||
"cloudflaredIsNotInstalled": "cloudflared is not installed",
|
||||
@@ -6709,6 +6710,7 @@
|
||||
"tunnelStarted": "Remote tunnel started",
|
||||
"tunnelStopped": "Remote tunnel stopped",
|
||||
"tunnelToken": "Tunnel token",
|
||||
"tunnelURL": "Tunnel URL:",
|
||||
"uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ",
|
||||
"uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.",
|
||||
"useExisting": "Use Existing",
|
||||
|
||||
@@ -6687,7 +6687,9 @@
|
||||
"installationFailed": "",
|
||||
"acceptRoutesHint": "",
|
||||
"shortLivedEnabledHint": "",
|
||||
"shortLivedTtlMsHint": ""
|
||||
"shortLivedTtlMsHint": "",
|
||||
"cloudflareTunnelURL": "",
|
||||
"tunnelURL": ""
|
||||
},
|
||||
"researchGlobal": {
|
||||
"advancedExternalSearchProviders": "",
|
||||
|
||||
@@ -6687,7 +6687,9 @@
|
||||
"installationFailed": "",
|
||||
"acceptRoutesHint": "",
|
||||
"shortLivedEnabledHint": "",
|
||||
"shortLivedTtlMsHint": ""
|
||||
"shortLivedTtlMsHint": "",
|
||||
"cloudflareTunnelURL": "",
|
||||
"tunnelURL": ""
|
||||
},
|
||||
"researchGlobal": {
|
||||
"advancedExternalSearchProviders": "",
|
||||
|
||||
@@ -6687,7 +6687,9 @@
|
||||
"installationFailed": "",
|
||||
"acceptRoutesHint": "",
|
||||
"shortLivedEnabledHint": "",
|
||||
"shortLivedTtlMsHint": ""
|
||||
"shortLivedTtlMsHint": "",
|
||||
"cloudflareTunnelURL": "",
|
||||
"tunnelURL": ""
|
||||
},
|
||||
"researchGlobal": {
|
||||
"advancedExternalSearchProviders": "",
|
||||
|
||||
@@ -6363,7 +6363,9 @@
|
||||
"installationFailed": "Falha na instalação",
|
||||
"acceptRoutesHint": "Padrão: desativado.",
|
||||
"shortLivedEnabledHint": "Padrão: desativado.",
|
||||
"shortLivedTtlMsHint": "Padrão: 900000 (15 minutos)."
|
||||
"shortLivedTtlMsHint": "Padrão: 900000 (15 minutos).",
|
||||
"cloudflareTunnelURL": "",
|
||||
"tunnelURL": ""
|
||||
},
|
||||
"worktrees": {
|
||||
"allowSilentSiblingBranchRenameDuringExecutorConflicts": " Permitir renomeação silenciosa de branch irmã durante conflitos do executor ",
|
||||
|
||||
@@ -6687,7 +6687,9 @@
|
||||
"installationFailed": "",
|
||||
"acceptRoutesHint": "",
|
||||
"shortLivedEnabledHint": "",
|
||||
"shortLivedTtlMsHint": ""
|
||||
"shortLivedTtlMsHint": "",
|
||||
"cloudflareTunnelURL": "",
|
||||
"tunnelURL": ""
|
||||
},
|
||||
"researchGlobal": {
|
||||
"advancedExternalSearchProviders": "",
|
||||
|
||||
@@ -6687,7 +6687,9 @@
|
||||
"installationFailed": "",
|
||||
"acceptRoutesHint": "",
|
||||
"shortLivedEnabledHint": "",
|
||||
"shortLivedTtlMsHint": ""
|
||||
"shortLivedTtlMsHint": "",
|
||||
"cloudflareTunnelURL": "",
|
||||
"tunnelURL": ""
|
||||
},
|
||||
"researchGlobal": {
|
||||
"advancedExternalSearchProviders": "",
|
||||
|
||||
21
packages/i18n/src/resources.d.ts
vendored
21
packages/i18n/src/resources.d.ts
vendored
@@ -1029,15 +1029,15 @@ export default interface Resources {
|
||||
"setupModeAriaLabel": "Agent setup mode",
|
||||
"showSystemAgents": "Show system agents",
|
||||
"skills": "Skills",
|
||||
"skillsDescription": "Manage the skills available to this agent.",
|
||||
"skillsDescription": "All enabled skills can be consulted automatically by any agent. Select skills below to force this agent to read them before starting work.",
|
||||
"skillsErrors_one": "{{count}} skill{{plural}} error{{pluralError}}",
|
||||
"skillsErrors_other": "{{count}} skill{{plural}} error{{pluralError}}",
|
||||
"skillsFound_one": "{{count}} skill{{plural}} found",
|
||||
"skillsFound_other": "{{count}} skill{{plural}} found",
|
||||
"skillsHint": "Optional skills to assign to this agent",
|
||||
"skillsHint": "All enabled skills can be consulted automatically by any agent. Select skills below to force this agent to read them before starting work.",
|
||||
"skillsImported_one": "{{count}} skill{{plural}} imported",
|
||||
"skillsImported_other": "{{count}} skill{{plural}} imported",
|
||||
"skillsNone": "No skills assigned",
|
||||
"skillsNone": "None",
|
||||
"skillsSelected_one": "{{count}} skill selected",
|
||||
"skillsSelected_other": "{{count}} skills selected",
|
||||
"skillsSkipped_one": "{{count}} skill{{plural}} skipped (already exist)",
|
||||
@@ -6690,6 +6690,7 @@ export default interface Resources {
|
||||
"authenticatedURL": "Authenticated URL:",
|
||||
"automaticallyRestoreTunnelOnStartupIfItWas": "Automatically restore tunnel on startup if it was running when last stopped. Default: disabled.",
|
||||
"cloudflare": "Cloudflare",
|
||||
"cloudflareTunnelURL": "Cloudflare tunnel URL:",
|
||||
"cloudflaredInstalled": "cloudflared installed successfully",
|
||||
"cloudflaredIsInstalled": "cloudflared is installed",
|
||||
"cloudflaredIsNotInstalled": "cloudflared is not installed",
|
||||
@@ -6746,6 +6747,7 @@ export default interface Resources {
|
||||
"tunnelStarted": "Remote tunnel started",
|
||||
"tunnelStopped": "Remote tunnel stopped",
|
||||
"tunnelToken": "Tunnel token",
|
||||
"tunnelURL": "Tunnel URL:",
|
||||
"uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ",
|
||||
"uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.",
|
||||
"useExisting": "Use Existing",
|
||||
@@ -7435,12 +7437,16 @@ export default interface Resources {
|
||||
"skills": {
|
||||
"addSkill": "Add a skill…",
|
||||
"allSkillsSelected": "All skills selected",
|
||||
"autoAvailable": "Auto-available",
|
||||
"autoAvailableTitle": "Enabled skills are available automatically.",
|
||||
"catalogSection": "Skills Catalog",
|
||||
"catalogUnavailable": "Catalog is temporarily unavailable. Please try again later.",
|
||||
"closeDetail": "Close skill detail",
|
||||
"closeView": "Close skills view",
|
||||
"disableSkill": "Disable {{name}}",
|
||||
"disabled": "Skill disabled",
|
||||
"disabledSkill": "Disabled",
|
||||
"disabledSkillTitle": "Disabled skills are not delivered to sessions, even when forced.",
|
||||
"discovered": "discovered",
|
||||
"discoveredCount_one": "{{count}} discovered skills",
|
||||
"discoveredCount_other": "{{count}} discovered skills",
|
||||
@@ -7448,6 +7454,9 @@ export default interface Resources {
|
||||
"enableSkill": "Enable {{name}}",
|
||||
"enabled": "Skill enabled",
|
||||
"filesLabel": "Files",
|
||||
"filter": "Filter skills",
|
||||
"forced": "Forced",
|
||||
"hiddenUnavailable": "Hidden skills include disabled or not-discovered entries",
|
||||
"install": "Install",
|
||||
"installError": "Failed to install skill",
|
||||
"installFailed": "Failed to install {{name}}: {{message}}",
|
||||
@@ -7458,19 +7467,25 @@ export default interface Resources {
|
||||
"loadCatalogError": "Failed to load catalog",
|
||||
"loadContentError": "Failed to load skill content",
|
||||
"loadDiscoveredError": "Failed to load discovered skills",
|
||||
"loadError": "Skills could not be loaded.",
|
||||
"loading": "Loading skills…",
|
||||
"loadingCatalog": "Loading catalog...",
|
||||
"loadingContent": "Loading skill content...",
|
||||
"loadingDiscovered": "Loading discovered skills...",
|
||||
"noCatalogAvailable": "No skills available in the catalog.",
|
||||
"noDiscovered": "No skills discovered in this project.",
|
||||
"noMatches": "No matching skills",
|
||||
"noMatchingDiscovered": "No discovered skills match your search.",
|
||||
"noMatchingSearch": "No skills match your search.",
|
||||
"noSkillMd": "(No SKILL.md found)",
|
||||
"noSkillsDiscovered": "No skills discovered",
|
||||
"notDiscovered": "Not discovered",
|
||||
"notDiscoveredTitle": "This stored skill is no longer discovered by the project.",
|
||||
"removeSkill": "Remove {{name}}",
|
||||
"searchLabel": "Search skills",
|
||||
"searchPlaceholder": "Search skills...",
|
||||
"skillStatePending": "Checking availability",
|
||||
"skillStatePendingTitle": "Skill discovery is loading or could not be refreshed.",
|
||||
"title": "Skills",
|
||||
"toggleError": "Failed to toggle skill",
|
||||
"toggleFailed": "Failed to toggle skill: {{message}}",
|
||||
|
||||
Reference in New Issue
Block a user