diff --git a/.changeset/fn-7882-copy-diagnostics-fix.md b/.changeset/fn-7882-copy-diagnostics-fix.md
new file mode 100644
index 0000000000..b0c6807762
--- /dev/null
+++ b/.changeset/fn-7882-copy-diagnostics-fix.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Fix "Copy diagnostics" crash on non-secure origins (mobile/HTTP).
+category: fix
+dev: Command Center System tab now routes diagnostics copy through copyTextToClipboard (secure-context guard + execCommand fallback) instead of navigator.clipboard.writeText, which was undefined outside secure contexts.
diff --git a/packages/dashboard/app/components/command-center/__tests__/SystemControlsArea.test.tsx b/packages/dashboard/app/components/command-center/__tests__/SystemControlsArea.test.tsx
index 29b922de92..7ceae7ef50 100644
--- a/packages/dashboard/app/components/command-center/__tests__/SystemControlsArea.test.tsx
+++ b/packages/dashboard/app/components/command-center/__tests__/SystemControlsArea.test.tsx
@@ -1,7 +1,7 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import "@testing-library/jest-dom";
import { CommandCenter } from "../CommandCenter";
@@ -9,6 +9,7 @@ const apiMock = vi.fn();
const mockFetchSystemInfo = vi.fn();
const mockFetchCurrentSystemRebuild = vi.fn();
const mockFetchSystemStats = vi.fn();
+const mockFetchSystemLogs = vi.fn();
const mockFetchNodeSystemStats = vi.fn();
const mockFetchGlobalSettings = vi.fn();
const mockFetchNodes = vi.fn();
@@ -26,7 +27,7 @@ vi.mock("../../../api/legacy", () => ({
fetchDashboardHealth: vi.fn().mockResolvedValue({ ok: true }),
fetchCurrentSystemRebuild: (...args: unknown[]) => mockFetchCurrentSystemRebuild(...args),
fetchSystemInfo: (...args: unknown[]) => mockFetchSystemInfo(...args),
- fetchSystemLogs: vi.fn().mockResolvedValue({ entries: [] }),
+ fetchSystemLogs: (...args: unknown[]) => mockFetchSystemLogs(...args),
reloadAllSystemPlugins: vi.fn().mockResolvedValue({ ok: true }),
requestSystemRestart: vi.fn().mockResolvedValue({ ok: true }),
restartAllSystemAgents: vi.fn().mockResolvedValue({ ok: true }),
@@ -114,10 +115,39 @@ function systemStatsFixture() {
}
describe("SystemControlsArea layout integration", () => {
+ const originalClipboard = navigator.clipboard;
+ const originalExecCommand = document.execCommand;
+
+ function mockClipboard(value: unknown) {
+ Object.defineProperty(navigator, "clipboard", {
+ configurable: true,
+ value,
+ });
+ }
+
+ function mockExecCommand(result: boolean) {
+ const execCommand = vi.fn().mockReturnValue(result);
+ Object.defineProperty(document, "execCommand", {
+ configurable: true,
+ value: execCommand,
+ });
+ return execCommand;
+ }
+
+ async function renderSystemTab(addToast = vi.fn()) {
+ render();
+ fireEvent.click(screen.getByTestId("command-center-tab-system"));
+ const diagnosticsCard = await screen.findByTestId("cc-syscontrol-diagnostics");
+ return { addToast, diagnosticsCard };
+ }
+
beforeEach(() => {
vi.clearAllMocks();
apiMock.mockImplementation((path: string) => Promise.resolve(emptyOverviewResponse(path)));
mockFetchSystemInfo.mockResolvedValue(systemInfoFixture());
+ mockFetchSystemLogs.mockResolvedValue({
+ entries: [{ timestamp: "2026-07-12T00:00:00.000Z", level: "info", message: "ready" }],
+ });
mockFetchCurrentSystemRebuild.mockResolvedValue({ job: null });
mockFetchSystemStats.mockResolvedValue(systemStatsFixture());
mockFetchNodeSystemStats.mockResolvedValue(systemStatsFixture());
@@ -125,6 +155,18 @@ describe("SystemControlsArea layout integration", () => {
mockFetchNodes.mockResolvedValue([]);
});
+ afterEach(() => {
+ Object.defineProperty(navigator, "clipboard", {
+ configurable: true,
+ value: originalClipboard,
+ });
+ Object.defineProperty(document, "execCommand", {
+ configurable: true,
+ value: originalExecCommand,
+ });
+ document.body.innerHTML = "";
+ });
+
it("wraps System controls, Server logs, and Live system health in the shared gap owner", async () => {
render();
@@ -162,6 +204,45 @@ describe("SystemControlsArea layout integration", () => {
expect(refresh.parentElement).toBe(header);
});
+ it("copies diagnostics through the execCommand fallback when Clipboard API is unavailable", async () => {
+ mockClipboard(undefined);
+ const execCommand = mockExecCommand(true);
+ const { addToast, diagnosticsCard } = await renderSystemTab();
+
+ fireEvent.click(within(diagnosticsCard).getByRole("button", { name: "Copy" }));
+
+ await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
+ expect(addToast).toHaveBeenCalledWith("Diagnostics copied to clipboard", "success");
+ expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("writeText"), "error");
+ });
+
+ it("copies diagnostics through navigator.clipboard.writeText in secure contexts", async () => {
+ const writeText = vi.fn().mockResolvedValue(undefined);
+ mockClipboard({ writeText });
+ const { addToast, diagnosticsCard } = await renderSystemTab();
+
+ fireEvent.click(within(diagnosticsCard).getByRole("button", { name: "Copy" }));
+
+ await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
+ expect(writeText.mock.calls[0]?.[0]).toContain('"recentLogs"');
+ expect(addToast).toHaveBeenCalledWith("Diagnostics copied to clipboard", "success");
+ expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("writeText"), "error");
+ });
+
+ it("shows a failure toast when diagnostics cannot be copied by either clipboard path", async () => {
+ mockFetchSystemInfo.mockResolvedValue({ ...systemInfoFixture(), logsSupported: false });
+ mockClipboard(undefined);
+ const execCommand = mockExecCommand(false);
+ const { addToast, diagnosticsCard } = await renderSystemTab();
+
+ fireEvent.click(within(diagnosticsCard).getByRole("button", { name: "Copy" }));
+
+ await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
+ expect(addToast).toHaveBeenCalledWith("Could not copy diagnostics to clipboard", "error");
+ expect(addToast).not.toHaveBeenCalledWith("Diagnostics copied to clipboard", "success");
+ expect(addToast).not.toHaveBeenCalledWith(expect.stringContaining("writeText"), "error");
+ });
+
it("keeps the System controls header row override active on mobile", () => {
const css = readFileSync(join(process.cwd(), "app/components/command-center/areas/SystemControlsArea.css"), "utf8");
diff --git a/packages/dashboard/app/components/command-center/areas/SystemControlsArea.tsx b/packages/dashboard/app/components/command-center/areas/SystemControlsArea.tsx
index 7c05283d40..ef714402ae 100644
--- a/packages/dashboard/app/components/command-center/areas/SystemControlsArea.tsx
+++ b/packages/dashboard/app/components/command-center/areas/SystemControlsArea.tsx
@@ -31,6 +31,7 @@ import {
} from "../../../api/legacy";
import { subscribeSse } from "../../../sse-bus";
import type { ToastType } from "../../../hooks/useToast";
+import { copyTextToClipboard } from "../../../utils/copyToClipboard";
import "./SystemControlsArea.css";
/*
@@ -356,8 +357,16 @@ export function SystemControlsArea({ projectId, addToast }: SystemControlsAreaPr
() =>
runAction("diagnostics", async () => {
const diagnostics = await buildDiagnostics();
- await navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2));
- toast(t("systemControls.diagnosticsCopied", "Diagnostics copied to clipboard"), "success");
+ /*
+ FNXC:SystemPanel 2026-07-12-00:00:
+ Diagnostics copy must use copyTextToClipboard because navigator.clipboard is undefined on non-secure origins such as mobile http://fusionstudio:4040. Calling writeText directly previously crashed with reading 'writeText' instead of surfacing a clear copy failure.
+ */
+ const copied = await copyTextToClipboard(JSON.stringify(diagnostics, null, 2));
+ if (copied) {
+ toast(t("systemControls.diagnosticsCopied", "Diagnostics copied to clipboard"), "success");
+ return;
+ }
+ toast(t("systemControls.diagnosticsCopyFailed", "Could not copy diagnostics to clipboard"), "error");
}),
[buildDiagnostics, runAction, t, toast],
);