diff --git a/.changeset/fn-7877-system-tab-spacing.md b/.changeset/fn-7877-system-tab-spacing.md new file mode 100644 index 0000000000..eba8d7731c --- /dev/null +++ b/.changeset/fn-7877-system-tab-spacing.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix cramped spacing on the Command Center System tab (logs and controls). +category: fix +dev: System tab now wraps SystemControlsArea + SystemStatsArea in a flex container so all sections share the --space-lg rhythm; adds a gap after Server logs. diff --git a/packages/dashboard/app/components/command-center/CommandCenter.css b/packages/dashboard/app/components/command-center/CommandCenter.css index 4f67b19c19..4f7674b956 100644 --- a/packages/dashboard/app/components/command-center/CommandCenter.css +++ b/packages/dashboard/app/components/command-center/CommandCenter.css @@ -123,10 +123,25 @@ Dashboard follows the global header rule: surface background, canonical height/p -webkit-overflow-scrolling: touch; } +/* +FNXC:SystemPanel 2026-07-12-16:10: +The System tab combines a controls fragment with a stats .cc-area. Keep the tabpanel as the only vertical scroll owner, and put the shared --space-lg section rhythm on this lightweight wrapper so the Server logs section has breathing room before Live system health. +*/ +.cc-system-tab { + display: flex; + flex-direction: column; + gap: var(--space-lg); + min-inline-size: 0; +} + @media (max-width: 768px) { .cc-tabpanel { padding-bottom: calc(var(--space-lg) + env(safe-area-inset-bottom, 0) + var(--standalone-bottom-gap)); } + + .cc-system-tab { + gap: var(--space-lg); + } } /* ---- Overview ---- */ diff --git a/packages/dashboard/app/components/command-center/CommandCenter.tsx b/packages/dashboard/app/components/command-center/CommandCenter.tsx index fc6e1170d4..c346819f35 100644 --- a/packages/dashboard/app/components/command-center/CommandCenter.tsx +++ b/packages/dashboard/app/components/command-center/CommandCenter.tsx @@ -595,10 +595,14 @@ export function CommandCenter({ live "on the dashboard under System where we have runtime metrics". */ return ( - <> +
+ {/* + FNXC:SystemPanel 2026-07-12-16:10: + The System tab renders a bare SystemControlsArea section fragment followed by SystemStatsArea's own .cc-area wrapper. The tab-level flex container owns the inter-area rhythm so controls, rebuild output, Server logs, and Live system health all share the same --space-lg gap without adding another scroll owner. + */} - +
); case "plugins": /* diff --git a/packages/dashboard/app/components/command-center/__tests__/SystemControlsArea.test.tsx b/packages/dashboard/app/components/command-center/__tests__/SystemControlsArea.test.tsx new file mode 100644 index 0000000000..2e56a096e3 --- /dev/null +++ b/packages/dashboard/app/components/command-center/__tests__/SystemControlsArea.test.tsx @@ -0,0 +1,157 @@ +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 "@testing-library/jest-dom"; +import { CommandCenter } from "../CommandCenter"; + +const apiMock = vi.fn(); +const mockFetchSystemInfo = vi.fn(); +const mockFetchCurrentSystemRebuild = vi.fn(); +const mockFetchSystemStats = vi.fn(); +const mockFetchNodeSystemStats = vi.fn(); +const mockFetchGlobalSettings = vi.fn(); +const mockFetchNodes = vi.fn(); + +vi.mock("../../../api/legacy", () => ({ + api: (path: string, opts?: RequestInit) => apiMock(path, opts), + withProjectId: (path: string, projectId?: string) => + projectId ? `${path}${path.includes("?") ? "&" : "?"}projectId=${encodeURIComponent(projectId)}` : path, + fetchOrgTree: vi.fn().mockResolvedValue([]), + fetchExecutorStats: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false, maxConcurrent: 2 }), + fetchSettings: vi.fn().mockResolvedValue({ maxConcurrent: 2, maxTriageConcurrent: 1, maxWorktrees: 5 }), + fetchConfig: vi.fn().mockResolvedValue({ maxConcurrent: 2, rootDir: "/" }), + updateSettings: vi.fn().mockResolvedValue({}), + createBackup: vi.fn().mockResolvedValue({ ok: true }), + fetchDashboardHealth: vi.fn().mockResolvedValue({ ok: true }), + fetchCurrentSystemRebuild: (...args: unknown[]) => mockFetchCurrentSystemRebuild(...args), + fetchSystemInfo: (...args: unknown[]) => mockFetchSystemInfo(...args), + fetchSystemLogs: vi.fn().mockResolvedValue({ entries: [] }), + reloadAllSystemPlugins: vi.fn().mockResolvedValue({ ok: true }), + requestSystemRestart: vi.fn().mockResolvedValue({ ok: true }), + restartAllSystemAgents: vi.fn().mockResolvedValue({ ok: true }), + restartSystemEngines: vi.fn().mockResolvedValue({ ok: true }), + startSystemRebuild: vi.fn().mockResolvedValue({ id: "job-1", status: "running", lines: [] }), +})); + +vi.mock("../../../api", () => ({ + fetchSystemStats: (...args: unknown[]) => mockFetchSystemStats(...args), + fetchNodeSystemStats: (...args: unknown[]) => mockFetchNodeSystemStats(...args), + fetchGlobalSettings: (...args: unknown[]) => mockFetchGlobalSettings(...args), + fetchNodes: (...args: unknown[]) => mockFetchNodes(...args), + killVitestProcesses: vi.fn().mockResolvedValue({ killed: 0, pids: [] }), + updateGlobalSettings: vi.fn().mockResolvedValue({}), +})); + +vi.mock("../../../sse-bus", () => ({ + subscribeSse: vi.fn(() => () => undefined), +})); + +vi.mock("../../../hooks/useAppSettings", () => ({ + useAppSettings: () => ({ + globalPaused: false, + enginePaused: false, + toggleGlobalPause: vi.fn(), + toggleEnginePause: vi.fn(), + refresh: vi.fn().mockResolvedValue(undefined), + }), +})); + +function emptyOverviewResponse(path: string) { + if (path.includes("/command-center/tokens")) { + return { totals: { totalTokens: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, nTasks: 0 }, cost: { usd: null, unavailable: true, stale: false }, groups: [] }; + } + if (path.includes("/command-center/tools")) return { totals: { calls: 0, errors: 0 }, groups: [] }; + if (path.includes("/command-center/activity")) return { totals: { agentRuns: 0, tasksDone: 0 }, daily: [] }; + if (path.includes("/command-center/signals")) return { totals: { open: 0, closed: 0 }, groups: [] }; + if (path.includes("/command-center/live")) return { activeTasks: 0, activeAgents: 0, openSignals: 0, tokensPerMinute: 0, tasksByColumn: [] }; + return {}; +} + +function systemInfoFixture() { + return { + pid: 12345, + nodeVersion: "v22.0.0", + platform: "darwin", + arch: "arm64", + sourceCheckout: true, + supervised: true, + engineRestartSupported: true, + agentRestartSupported: true, + pluginReloadSupported: true, + logsSupported: true, + activeRebuild: null, + }; +} + +function systemStatsFixture() { + return { + systemStats: { + rss: 1024, + heapUsed: 512, + heapTotal: 1024, + heapLimit: 2048, + external: 0, + arrayBuffers: 0, + cpuPercent: 10, + loadAvg: [0.1, 0.2, 0.3] as [number, number, number], + cpuCount: 8, + systemTotalMem: 4096, + systemFreeMem: 2048, + pid: 12345, + nodeVersion: "v22.0.0", + platform: "darwin/arm64", + }, + taskStats: { + total: 0, + byColumn: {}, + active: 0, + agents: { idle: 0, active: 0, running: 0, error: 0 }, + }, + vitestProcessCount: 0, + vitestLastAutoKillAt: null, + }; +} + +describe("SystemControlsArea layout integration", () => { + beforeEach(() => { + vi.clearAllMocks(); + apiMock.mockImplementation((path: string) => Promise.resolve(emptyOverviewResponse(path))); + mockFetchSystemInfo.mockResolvedValue(systemInfoFixture()); + mockFetchCurrentSystemRebuild.mockResolvedValue({ job: null }); + mockFetchSystemStats.mockResolvedValue(systemStatsFixture()); + mockFetchNodeSystemStats.mockResolvedValue(systemStatsFixture()); + mockFetchGlobalSettings.mockResolvedValue({ vitestAutoKillEnabled: true, vitestKillThresholdPct: 90 }); + mockFetchNodes.mockResolvedValue([]); + }); + + it("wraps System controls, Server logs, and Live system health in the shared gap owner", async () => { + render(); + + fireEvent.click(screen.getByTestId("command-center-tab-system")); + + const systemTab = await screen.findByTestId("cc-system-tab"); + const controls = await screen.findByTestId("cc-system-controls"); + const logs = await screen.findByTestId("cc-system-logs"); + const stats = await screen.findByTestId("cc-area-system"); + + expect(systemTab).toHaveClass("cc-system-tab"); + expect(systemTab).toContainElement(controls); + expect(systemTab).toContainElement(logs); + expect(systemTab).toContainElement(stats); + expect(controls.parentElement).toBe(systemTab); + expect(logs.parentElement).toBe(systemTab); + expect(stats.parentElement).toBe(systemTab); + expect(screen.getByTestId("cc-system-logs-toggle")).toBeInTheDocument(); + await waitFor(() => expect(mockFetchSystemInfo).toHaveBeenCalled()); + }); + + it("keeps the System tab gap and mobile scroll-owner CSS contracts tokenized", () => { + const css = readFileSync(join(process.cwd(), "app/components/command-center/CommandCenter.css"), "utf8"); + + expect(css).toMatch(/\.cc-system-tab\s*{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*gap:\s*var\(--space-lg\);/s); + expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*{[\s\S]*\.cc-tabpanel\s*{[^}]*padding-bottom:\s*calc\(var\(--space-lg\) \+ env\(safe-area-inset-bottom, 0\) \+ var\(--standalone-bottom-gap\)\);/); + expect(css).toMatch(/@media\s*\(max-width:\s*768px\)\s*{[\s\S]*\.cc-system-tab\s*{[^}]*gap:\s*var\(--space-lg\);/); + expect(css).not.toMatch(/\.cc-system-tab\s*{[^}]*overflow-y:\s*auto;/s); + }); +}); diff --git a/packages/dashboard/app/components/command-center/areas/SystemControlsArea.css b/packages/dashboard/app/components/command-center/areas/SystemControlsArea.css index a464194ac3..dcda183d6a 100644 --- a/packages/dashboard/app/components/command-center/areas/SystemControlsArea.css +++ b/packages/dashboard/app/components/command-center/areas/SystemControlsArea.css @@ -71,7 +71,7 @@ never grows the page; the page must not scroll horizontally (pre wraps). .cc-syscontrols-banner--error { justify-content: space-between; - border-color: var(--danger, #f87171); + border-color: var(--color-error); } .cc-syscontrols-job-status { @@ -84,7 +84,7 @@ never grows the page; the page must not scroll horizontally (pre wraps). } .cc-syscontrols-job-status--failed { - color: var(--danger, #f87171); + color: var(--color-error); } .cc-syscontrols-output { @@ -115,11 +115,11 @@ never grows the page; the page must not scroll horizontally (pre wraps). } .cc-syscontrols-log-line--warn .cc-syscontrols-log-message { - color: var(--warning, #facc15); + color: var(--color-warning); } .cc-syscontrols-log-line--error .cc-syscontrols-log-message { - color: var(--danger, #f87171); + color: var(--color-error); } .cc-syscontrols-log-time {