From 1d860ec310a62c069f6b2389fe52a06e3422efa6 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 25 Jun 2026 22:19:24 -0700 Subject: [PATCH 1/2] feat: global concurrency slider (footer + dashboard) and scoped settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Global Max Concurrent" slider to the footer engine menu and the Command Center Concurrency card, and group the Scheduling settings by Global vs Project scope so the global cap isn't mistaken for a per-project setting (clearer on mobile). Both sliders are backed by a single shared `useGlobalConcurrency` hook (module-level store) so they read/write one source of truth and revalidate after every PUT /api/global-concurrency — fixing the last-writer-wins and stale-clobber races a per-component cache would cause. The hook treats a fetch error as non-interactive (slider disabled, not stuck at 1), surfaces a save-state indicator, and flushes a pending edit on menu close / unmount so a quick drag-then-dismiss is never silently dropped. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...l-concurrency-slider-and-scope-grouping.md | 7 + .../app/components/EngineControlMenu.css | 14 ++ .../app/components/EngineControlMenu.tsx | 45 +++++ .../app/components/SettingsModal.css | 49 +++++ .../command-center/CommandCenterControls.css | 12 ++ .../command-center/CommandCenterControls.tsx | 40 ++++ .../settings/sections/SchedulingSection.tsx | 22 +++ .../app/hooks/useGlobalConcurrency.ts | 182 ++++++++++++++++++ 8 files changed, 371 insertions(+) create mode 100644 .changeset/global-concurrency-slider-and-scope-grouping.md create mode 100644 packages/dashboard/app/hooks/useGlobalConcurrency.ts 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..91574df108 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,16 @@ 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..af4979e2c1 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,16 @@ 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. + const globalSaveLabel = gc.status === "loading" || gc.status === "idle" + ? t("commandCenter.controls.status.loading", "Loading…") + : 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 +253,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. + */} +