diff --git a/.changeset/global-concurrency-slider-and-scope-grouping.md b/.changeset/global-concurrency-slider-and-scope-grouping.md new file mode 100644 index 0000000000..4c02c88be9 --- /dev/null +++ b/.changeset/global-concurrency-slider-and-scope-grouping.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Adjust the global concurrency cap from the footer and dashboard; settings grouped by global vs project scope. +category: feature +dev: Added a Global Max Concurrent slider (wired to fetch/updateGlobalConcurrency) to EngineControlMenu (footer) and the dashboard CommandCenterControls Concurrency card, with debounced saves matching the existing project sliders. SchedulingSection now groups fields under labeled "Global — all projects" and "This project" subheadings with scope badges so the global cap is not mistaken for a per-project setting (clearer on mobile). diff --git a/packages/dashboard/app/components/EngineControlMenu.css b/packages/dashboard/app/components/EngineControlMenu.css index efe0ee9e0f..6738a0c859 100644 --- a/packages/dashboard/app/components/EngineControlMenu.css +++ b/packages/dashboard/app/components/EngineControlMenu.css @@ -71,6 +71,14 @@ font-weight: 500; } +/* FNXC:GlobalConcurrencyControls 2026-06-25-22:45: "All projects" scope caption sits between the global-cap title and its save-state indicator; muted so the save-state remains the emphasized signal. */ +.engine-control-menu__scope-caption { + margin-inline-start: auto; + color: var(--text-muted); + font-size: var(--font-size-xs); + font-weight: 500; +} + .engine-control-menu__save-state--saving { color: var(--color-warning); } @@ -84,6 +92,12 @@ color: var(--color-error); } +/* FNXC:GlobalConcurrencyControls 2026-06-25-14:10: The global cap is a cross-project setting; separate it visually from the per-project sliders below with a divider and an "All projects" caption. */ +.engine-control-menu__section--global { + padding-bottom: var(--space-sm); + border-bottom: 1px solid var(--border); +} + .engine-control-menu__slider { display: flex; flex-direction: column; diff --git a/packages/dashboard/app/components/EngineControlMenu.tsx b/packages/dashboard/app/components/EngineControlMenu.tsx index 250e343f8f..a0b5383c1c 100644 --- a/packages/dashboard/app/components/EngineControlMenu.tsx +++ b/packages/dashboard/app/components/EngineControlMenu.tsx @@ -5,6 +5,8 @@ import { DEFAULT_PROJECT_SETTINGS } from "@fusion/core"; import { Pause, Play, SlidersHorizontal, Square } from "lucide-react"; import { fetchConfig, fetchSettings, updateSettings } from "../api/legacy"; import { useAppSettings } from "../hooks/useAppSettings"; +// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Footer menu adopts the shared global-concurrency hook so it and the Command Center card read/write ONE source of truth (no more duplicated fetch/debounce/clobber logic). +import { useGlobalConcurrency } from "../hooks/useGlobalConcurrency"; export interface EngineControlMenuHandle { open: () => void; @@ -70,6 +72,8 @@ export const EngineControlMenu = forwardRef>({ status: "idle", data: null, error: null }); const [concurrencyDirty, setConcurrencyDirty] = useState(false); const [concurrencySaveState, setConcurrencySaveState] = useState<"idle" | "saving" | "saved" | "error">("idle"); + // FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Fetch is gated on the menu being open; the hook flushes any pending debounced write when `open` flips false. + const gc = useGlobalConcurrency({ activeWhen: open }); const closeMenu = useCallback(() => setOpen(false), []); const openMenu = useCallback(() => setOpen(true), []); @@ -167,6 +171,19 @@ export const EngineControlMenu = forwardRef + {/* + FNXC:GlobalConcurrencyControls 2026-06-25-14:10: + Operators need to adjust the global cross-project concurrency cap from the footer engine menu and the dashboard Concurrency card, not just the Settings modal; global cap is distinct from per-project maxConcurrent and persists via the central /api/global-concurrency endpoint. + */} +
+
+ {t("settings.scheduling.globalMaxConcurrent", "Global Max Concurrent")} + {t("commandCenter.controls.scope.allProjects", "All projects")} + + {globalSaveLabel} + +
+ + {gc.status === "error" ?

{t("commandCenter.controls.concurrency.error", "Unable to load concurrency settings")}

: null} +
+
{t("commandCenter.controls.concurrency.title", "Concurrency")} diff --git a/packages/dashboard/app/components/SettingsModal.css b/packages/dashboard/app/components/SettingsModal.css index 8ab9bd73e1..5295ecd776 100644 --- a/packages/dashboard/app/components/SettingsModal.css +++ b/packages/dashboard/app/components/SettingsModal.css @@ -1778,6 +1778,55 @@ Settings section headings should preserve hierarchy through spacing and type onl color: var(--text-muted); } +/* +FNXC:SettingsScopeGrouping 2026-06-25-10:42: +Mobile settings must make clear which controls are global (all projects) vs project-scoped; group scheduling fields under labeled Global/This-project subheadings with a scope badge so operators don't mistake the global concurrency cap for a per-project setting. +The header row wraps so the badge drops below the heading on narrow widths instead of overflowing. +*/ +.settings-scope-group { + margin-top: var(--space-md); +} + +.settings-scope-group-header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-sm); +} + +.settings-scope-group-header .settings-section-heading { + padding-bottom: 0; + margin-bottom: 0; +} + +.settings-scope-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; + white-space: nowrap; +} + +.settings-scope-badge--global { + background-color: color-mix(in srgb, var(--accent, #4a90e2) 18%, transparent); + color: var(--accent, #4a90e2); +} + +.settings-scope-badge--project { + background-color: color-mix(in srgb, var(--text-muted) 15%, transparent); + color: var(--text-muted); +} + +.settings-scope-caption { + display: block; + margin: var(--space-xs) 0 var(--space-md); + color: var(--text-muted); + font-size: 12px; + line-height: 1.4; +} + .settings-description { font-size: 13px; color: var(--text-dim); diff --git a/packages/dashboard/app/components/command-center/CommandCenterControls.css b/packages/dashboard/app/components/command-center/CommandCenterControls.css index d3e382a7a9..323b836712 100644 --- a/packages/dashboard/app/components/command-center/CommandCenterControls.css +++ b/packages/dashboard/app/components/command-center/CommandCenterControls.css @@ -90,6 +90,18 @@ font-size: 0.8125rem; } +/* FNXC:GlobalConcurrencyControls 2026-06-25-14:10: The global cap is a cross-project setting; span it full-width at the top of the Concurrency card and separate it from the per-project sliders with a divider plus an "Across all projects" caption. */ +.cc-controls-slider--global { + grid-column: 1 / -1; + padding-bottom: var(--space-md); + border-bottom: 1px solid var(--border); +} + +.cc-controls-slider-caption { + color: var(--text-muted); + font-size: 0.75rem; +} + .cc-controls-slider-label { display: flex; align-items: center; diff --git a/packages/dashboard/app/components/command-center/CommandCenterControls.tsx b/packages/dashboard/app/components/command-center/CommandCenterControls.tsx index ed750a6bf2..fa63230378 100644 --- a/packages/dashboard/app/components/command-center/CommandCenterControls.tsx +++ b/packages/dashboard/app/components/command-center/CommandCenterControls.tsx @@ -4,6 +4,8 @@ import { Power } from "lucide-react"; import { DEFAULT_PROJECT_SETTINGS, type ColorTheme, type ThemeMode } from "@fusion/core"; import { fetchConfig, fetchSettings, updateSettings } from "../../api/legacy"; import { useAppSettings } from "../../hooks/useAppSettings"; +// FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Concurrency card adopts the shared global-concurrency hook so it and the footer EngineControlMenu read/write ONE source of truth (no more duplicated fetch/debounce/clobber logic). +import { useGlobalConcurrency } from "../../hooks/useGlobalConcurrency"; import { ThemeDropdown } from "../ThemeDropdown"; import type { TaskView } from "../../hooks/useViewState"; import "./CommandCenterControls.css"; @@ -79,6 +81,8 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn const [concurrencyState, setConcurrencyState] = useState>({ status: "loading", data: null, error: null }); const [concurrencyDirty, setConcurrencyDirty] = useState(false); const [concurrencySaveState, setConcurrencySaveState] = useState<"idle" | "saving" | "saved" | "error">("idle"); + // FNXC:GlobalConcurrencyControls 2026-06-25-22:45: No activeWhen — the card is mounted only while visible, so it fetches on mount and flushes pending writes on unmount via the shared hook. + const gc = useGlobalConcurrency(); useEffect(() => { let cancelled = false; @@ -145,6 +149,19 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn const effectiveGlobalPaused = globalPaused; const concurrencyValues = concurrencyState.data ?? DEFAULT_CONCURRENCY_VALUES; + // FNXC:GlobalConcurrencyControls 2026-06-25-22:45: Mirror the per-project slider save-state labels for the shared global cap. + // FNXC:GlobalConcurrencyControls 2026-06-26-06:05: Explicit load-error branch — a failed initial load leaves saveState "idle", so the label otherwise fell through to "Ready" while the slider was disabled and an error alert shown. + const globalSaveLabel = gc.status === "loading" || gc.status === "idle" + ? t("commandCenter.controls.status.loading", "Loading…") + : gc.status === "error" + ? t("commandCenter.controls.status.loadError", "Load failed") + : gc.saveState === "saving" + ? t("commandCenter.controls.status.saving", "Saving…") + : gc.saveState === "saved" + ? t("commandCenter.controls.status.saved", "Saved") + : gc.saveState === "error" + ? t("commandCenter.controls.status.saveError", "Save failed") + : t("commandCenter.controls.status.ready", "Ready"); /* FNXC:CommandCenter 2026-06-20-00:20: @@ -239,6 +256,32 @@ export function CommandCenterControls({ projectId, colorTheme, themeMode, shadcn
+ {/* + FNXC:GlobalConcurrencyControls 2026-06-25-14:10: + Operators need to adjust the global cross-project concurrency cap from the footer engine menu and the dashboard Concurrency card, not just the Settings modal; global cap is distinct from per-project maxConcurrent and persists via the central /api/global-concurrency endpoint. + */} +