FN-7061: Graduate Remote Access settings
Remote Access settings are now always available while tunnel prerequisites remain gated. - Remove Remote Access from experimental feature toggles and section visibility gates. - Keep legacy remoteAccess experimental flags hidden so upgrades cannot disable the section. - Update settings and remote access docs plus release notes for the graduated UI. - Cover always-visible Remote Access behavior across missing and legacy experimental flag states. Files changed: .changeset/fn-7061-remote-access-graduation.md | 7 +++ docs/remote-access.md | 2 +- docs/settings-reference.md | 1 + .../dashboard/app/components/SettingsModal.tsx | 17 ++---- .../SettingsModal.remote-notifications.test.tsx | 13 +++++ .../SettingsModal.scheduling-merge.test.tsx | 60 ++++++++++++---------- 6 files changed, 60 insertions(+), 40 deletions(-) Fusion-Task-Id: FN-7061 Fusion-Task-Lineage: 2e6a6977-b43b-4c7b-8100-0f2f2f018fcf
This commit is contained in:
7
.changeset/fn-7061-remote-access-graduation.md
Normal file
7
.changeset/fn-7061-remote-access-graduation.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Make Remote Access settings visible without enabling an experimental flag.
|
||||
category: feature
|
||||
dev: Graduates the Settings UI section while leaving remoteAccess provider/token gating unchanged.
|
||||
@@ -22,7 +22,7 @@ It documents only behavior implemented in the current codebase:
|
||||
## 1.1 General requirements
|
||||
|
||||
- Remote Access is **global-scoped** (`GlobalSettings.remoteAccess` in `packages/core/src/types.ts`).
|
||||
- Configure it in dashboard settings (Remote tab) or via `PUT /api/settings/global` or `PUT /api/remote/settings`.
|
||||
- Configure it in dashboard settings (Remote Access tab, always visible) or via `PUT /api/settings/global` or `PUT /api/remote/settings`.
|
||||
- A provider must be selected (`remoteAccess.activeProvider`) before tunnel start.
|
||||
- Start/stop is always manual through `/api/remote/tunnel/start` and `/api/remote/tunnel/stop`.
|
||||
|
||||
|
||||
@@ -779,6 +779,7 @@ See also:
|
||||
### Remote Access settings (global-scoped)
|
||||
|
||||
Remote access settings are global-only (stored in `~/.fusion/settings.json`), not project-scoped.
|
||||
The Settings → Remote Access section is always visible; starting a tunnel still requires `remoteAccess.enabled`, a selected provider, and provider-specific prerequisites.
|
||||
The canonical persisted shape is a nested `remoteAccess` object.
|
||||
|
||||
Use **[Remote Access runbook](./remote-access.md)** for setup prerequisites (Tailscale/Cloudflare), tokenized login-link security caveats, and operational troubleshooting. Keep this section as a schema reference.
|
||||
|
||||
@@ -272,7 +272,6 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
|
||||
const KNOWN_EXPERIMENTAL_FEATURES: Record<string, string> = {
|
||||
insights: "Insights",
|
||||
memoryView: "Memory Editor",
|
||||
remoteAccess: "Remote Access",
|
||||
skillsView: "Skills View",
|
||||
nodesView: "Nodes View",
|
||||
devServerView: "Dev Server",
|
||||
@@ -304,12 +303,16 @@ Right Dock Panel is no longer experimental: keep honoring the dock as always-on
|
||||
|
||||
FNXC:SettingsExperimental 2026-06-23-01:31:
|
||||
Chat Rooms, Goals, Memory, Insights, Skills, and Todo graduated from Experimental. Hide stale persisted flags so users cannot accidentally disable now-default dashboard surfaces during upgrades.
|
||||
|
||||
FNXC:SettingsExperimental 2026-06-26-00:00:
|
||||
Remote Access graduated from Experimental — section is always available; stale persisted `remoteAccess` flags are hidden so upgrades cannot disable it.
|
||||
*/
|
||||
const HIDDEN_EXPERIMENTAL_FEATURE_KEYS = new Set<string>([
|
||||
"chatRooms",
|
||||
"goalsView",
|
||||
"insights",
|
||||
"memoryView",
|
||||
"remoteAccess",
|
||||
"roadmap",
|
||||
"rightDock",
|
||||
"skillsView",
|
||||
@@ -816,14 +819,9 @@ export function SettingsModal({
|
||||
|
||||
const { nodes } = useNodes();
|
||||
const experimentalFeatures = form.experimentalFeatures ?? {};
|
||||
const remoteAccessEnabled = isExperimentalFeatureEnabled(experimentalFeatures, "remoteAccess");
|
||||
const researchViewEnabled = isExperimentalFeatureEnabled(experimentalFeatures, "researchView");
|
||||
const evalsViewEnabled = isExperimentalFeatureEnabled(experimentalFeatures, "evalsView");
|
||||
const visibleSections = SETTINGS_SECTIONS.filter((section) => {
|
||||
if (section.id === "remote") {
|
||||
return remoteAccessEnabled;
|
||||
}
|
||||
|
||||
if (section.id === "research-global" || section.id === "research-project") {
|
||||
return researchViewEnabled;
|
||||
}
|
||||
@@ -842,11 +840,6 @@ export function SettingsModal({
|
||||
const activeSectionScope = visibleSections.find((s) => s.id === activeSection)?.scope;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSection === "remote" && !remoteAccessEnabled) {
|
||||
setActiveSection(firstVisibleSectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((activeSection === "research-global" || activeSection === "research-project") && !researchViewEnabled) {
|
||||
setActiveSection(firstVisibleSectionId);
|
||||
return;
|
||||
@@ -860,7 +853,7 @@ export function SettingsModal({
|
||||
if (!visibleSections.some((section) => section.id === activeSection)) {
|
||||
setActiveSection(firstVisibleSectionId);
|
||||
}
|
||||
}, [activeSection, remoteAccessEnabled, researchViewEnabled, evalsViewEnabled, firstVisibleSectionId, visibleSections]);
|
||||
}, [activeSection, researchViewEnabled, evalsViewEnabled, firstVisibleSectionId, visibleSections]);
|
||||
|
||||
// Auth state (independent of the settings save flow)
|
||||
const [authProviders, setAuthProviders] = useState<AuthProvider[]>([]);
|
||||
|
||||
@@ -220,6 +220,19 @@ describe("SettingsModal", () => {
|
||||
await user.click(summary);
|
||||
};
|
||||
|
||||
it("opens the Remote Access section when the legacy experimental flag is absent", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {},
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
await openRemoteSection();
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Remote Access" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("with default Remote render", () => {
|
||||
beforeEach(async () => {
|
||||
renderModal();
|
||||
|
||||
@@ -1388,53 +1388,59 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
|
||||
describe("section visibility behind experimental flags", () => {
|
||||
it("hides Remote Access nav item when experimentalFeatures.remoteAccess is falsy", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {},
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.queryByRole("button", { name: /Remote Access/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Remote Access nav item when experimentalFeatures.remoteAccess is true", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { remoteAccess: true },
|
||||
});
|
||||
it.each([
|
||||
["experimentalFeatures undefined", undefined],
|
||||
["experimentalFeatures empty", {}],
|
||||
["legacy remoteAccess false", { remoteAccess: false }],
|
||||
["legacy remoteAccess true", { remoteAccess: true }],
|
||||
] as const)("shows Remote Access nav item when %s", async (_label, experimentalFeatures) => {
|
||||
const { experimentalFeatures: _omitted, ...settingsWithoutExperimentalFeatures } = defaultSettings;
|
||||
mockFetchSettings.mockResolvedValue(
|
||||
experimentalFeatures === undefined
|
||||
? settingsWithoutExperimentalFeatures
|
||||
: {
|
||||
...defaultSettings,
|
||||
experimentalFeatures,
|
||||
},
|
||||
);
|
||||
|
||||
renderModal();
|
||||
|
||||
expect(await screen.findByRole("button", { name: /Remote Access/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Remote Access in KNOWN_EXPERIMENTAL_FEATURES toggle list", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {},
|
||||
});
|
||||
it.each([
|
||||
["experimentalFeatures undefined", undefined],
|
||||
["experimentalFeatures empty", {}],
|
||||
["legacy remoteAccess false", { remoteAccess: false }],
|
||||
["legacy remoteAccess true", { remoteAccess: true }],
|
||||
] as const)("does not render a Remote Access experimental toggle when %s", async (_label, experimentalFeatures) => {
|
||||
const { experimentalFeatures: _omitted, ...settingsWithoutExperimentalFeatures } = defaultSettings;
|
||||
mockFetchSettings.mockResolvedValue(
|
||||
experimentalFeatures === undefined
|
||||
? settingsWithoutExperimentalFeatures
|
||||
: {
|
||||
...defaultSettings,
|
||||
experimentalFeatures,
|
||||
},
|
||||
);
|
||||
|
||||
renderModal();
|
||||
|
||||
await openExperimentalFeaturesSection();
|
||||
|
||||
expect(screen.getByLabelText("Remote Access")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Remote Access")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to the first selectable section when opening remote while remoteAccess is disabled", async () => {
|
||||
it("stays on the remote section when opening remote while remoteAccess is absent", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {},
|
||||
});
|
||||
|
||||
renderModal({ initialSection: "remote" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.queryByRole("button", { name: /Remote Access/i })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "General" })).toBeInTheDocument();
|
||||
expect(await screen.findByRole("heading", { name: "Remote Access" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides research settings nav items when experimentalFeatures.researchView is disabled", async () => {
|
||||
|
||||
Reference in New Issue
Block a user