FN-7877: fix Command Center System tab spacing and canonicalize status colors
Wraps the System tab's controls and stats areas in a shared flex container so vertical rhythm is consistent, and canonicalizes hard-coded status colors to design tokens. - Wrap SystemControlsArea + SystemStatsArea in a new .cc-system-tab flex container with --space-lg gap, replacing the bare React fragment, so Server logs and Live system health sections get consistent breathing room. - Add responsive gap rule for .cc-system-tab under the 768px breakpoint. - Replace hard-coded --danger/--warning fallback colors in SystemControlsArea.css with canonical --color-error/--color-warning tokens. - Add SystemControlsArea test coverage. - Add a patch changeset documenting the spacing fix. Files changed: .changeset/fn-7877-system-tab-spacing.md | 7 + .../components/command-center/CommandCenter.css | 15 ++ .../components/command-center/CommandCenter.tsx | 8 +- .../__tests__/SystemControlsArea.test.tsx | 157 +++++++++++++++++++++ .../command-center/areas/SystemControlsArea.css | 8 +- 5 files changed, 189 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-7877 Fusion-Task-Lineage: eb4a97ed-5442-43e8-8d38-348fc3e220a8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7877-system-tab-spacing.md
Normal file
7
.changeset/fn-7877-system-tab-spacing.md
Normal file
@@ -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.
|
||||
@@ -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 ---- */
|
||||
|
||||
@@ -595,10 +595,14 @@ export function CommandCenter({
|
||||
live "on the dashboard under System where we have runtime metrics".
|
||||
*/
|
||||
return (
|
||||
<>
|
||||
<div className="cc-system-tab" data-testid="cc-system-tab">
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
<SystemControlsArea projectId={projectId} addToast={addToast} />
|
||||
<SystemStatsArea />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
case "plugins":
|
||||
/*
|
||||
|
||||
@@ -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(<CommandCenter projectId="proj-1" />);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user