From deba4c32f2fde89cfe75be6321446f82a4384968 Mon Sep 17 00:00:00 2001 From: dualchimerra Date: Sun, 20 Sep 2026 14:43:08 +0200 Subject: [PATCH] fix(web): harden settings and live project updates --- .../web/src/components/MonitorDashboard.tsx | 2 +- studio/web/src/pages/project/Overview.tsx | 14 ++++++++-- .../pages/project/steps/PreprocessInpaint.tsx | 5 +++- studio/web/src/pages/queue/DataJobsPanel.tsx | 13 +++++++-- studio/web/src/pages/tools/Settings.tsx | 28 +++++++++++++++++-- 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/studio/web/src/components/MonitorDashboard.tsx b/studio/web/src/components/MonitorDashboard.tsx index 4ffacfc..1b50710 100644 --- a/studio/web/src/components/MonitorDashboard.tsx +++ b/studio/web/src/components/MonitorDashboard.tsx @@ -531,7 +531,7 @@ export function EvalMetricsPanel({ state, connected, taskId }: { } finally { setRunning(false) } - }, [pid, vid, taskId, selected, load]) + }, [pid, vid, taskId, selected, load, t]) const latestByKey = useMemo(() => { const out: Partial> = {} diff --git a/studio/web/src/pages/project/Overview.tsx b/studio/web/src/pages/project/Overview.tsx index 98d05da..2348560 100644 --- a/studio/web/src/pages/project/Overview.tsx +++ b/studio/web/src/pages/project/Overview.tsx @@ -10,7 +10,7 @@ * Live 训练进度 (step/total/ETA) 不实装 —— 需 SSE/monitor state 整合,留 follow-up。 * "复制配置开新版本" / "调小 batch 重训" 需新后端 API,渲染为占位按钮 toast 提示。 */ -import { useEffect, useMemo, useState, type ReactNode } from 'react' +import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate, useOutletContext } from 'react-router-dom' import { @@ -33,6 +33,7 @@ import { arBucket } from '../../lib/aspectRatio' import { computePixelHist } from '../../lib/pixelBins' import { useProjectCtx } from '../../context/ProjectContext' import { useToast } from '../../components/Toast' +import { useEventStream } from '../../lib/useEventStream' type OverviewTab = 'details' | 'tasks' | 'output' @@ -1102,7 +1103,7 @@ export default function ProjectOverview() { // 项目全部 task(最近优先)— 右栏 Recent runs + banner / Output 数据源 const [projTasks, setProjTasks] = useState([]) - useEffect(() => { + const loadProjectTasks = useCallback(() => { let cancelled = false void api.listQueue() .then((items) => { @@ -1115,6 +1116,15 @@ export default function ProjectOverview() { .catch(() => { if (!cancelled) setProjTasks([]) }) return () => { cancelled = true } }, [project.id]) + useEffect(() => loadProjectTasks(), [loadProjectTasks]) + + useEventStream((evt) => { + if (evt.type === 'train_loop_started' + || evt.type === 'auto_epoch_backup_written' + || evt.type === 'task_state_changed') { + loadProjectTasks() + } + }) const latestTask = useMemo( () => projTasks.find((tk) => tk.version_id === selectedVid) ?? null, diff --git a/studio/web/src/pages/project/steps/PreprocessInpaint.tsx b/studio/web/src/pages/project/steps/PreprocessInpaint.tsx index 19d008f..c044c25 100644 --- a/studio/web/src/pages/project/steps/PreprocessInpaint.tsx +++ b/studio/web/src/pages/project/steps/PreprocessInpaint.tsx @@ -110,7 +110,10 @@ export default function PreprocessInpaintPage() { () => images.find((im) => im.name === activeName) ?? null, [images, activeName], ) - const activeHistory = activeName ? (historyByImage[activeName] ?? []) : [] + const activeHistory = useMemo( + () => activeName ? (historyByImage[activeName] ?? []) : [], + [activeName, historyByImage], + ) const activeRedo = activeName ? (redoByImage[activeName] ?? []) : [] const activePaintStrokes = useMemo( () => activeHistory.filter((h) => h.kind === 'paint').map((h) => h.stroke), diff --git a/studio/web/src/pages/queue/DataJobsPanel.tsx b/studio/web/src/pages/queue/DataJobsPanel.tsx index 67f8ff2..d648542 100644 --- a/studio/web/src/pages/queue/DataJobsPanel.tsx +++ b/studio/web/src/pages/queue/DataJobsPanel.tsx @@ -121,9 +121,18 @@ export default function DataJobsPanel({ paused: t('status.paused'), scheduled: t('status.scheduled'), } return ( - + ) } diff --git a/studio/web/src/pages/tools/Settings.tsx b/studio/web/src/pages/tools/Settings.tsx index 123a26e..d22449f 100644 --- a/studio/web/src/pages/tools/Settings.tsx +++ b/studio/web/src/pages/tools/Settings.tsx @@ -220,6 +220,27 @@ const EMPTY: Secrets = { } } +/** Keep the settings screen compatible with older secrets payloads. + * + * New sections are added over time, while an already running backend or a + * test fixture can still return the previous shape. The UI must not crash + * during that short mismatch. Secrets are only two levels deep, so a + * shallow merge per section is sufficient and preserves server values. + */ +function withSecretsDefaults(value: Secrets): Secrets { + const merged = { ...EMPTY, ...value } as Record + for (const key of Object.keys(EMPTY) as (keyof Secrets)[]) { + const fallback = EMPTY[key] + const actual = value[key] + if (fallback && actual + && typeof fallback === 'object' && !Array.isArray(fallback) + && typeof actual === 'object' && !Array.isArray(actual)) { + merged[key as string] = { ...fallback, ...actual } + } + } + return merged as unknown as Secrets +} + const textInputClass = 'w-full px-2 py-1 outline-none rounded-sm bg-sunken border border-subtle text-sm text-fg-primary focus:border-accent' const MODEL_DESCRIPTION_KEYS: Record = { @@ -242,7 +263,7 @@ export default function SettingsPage() { // 本组件 mount/unmount(抽屉开关)不再触发重拉。`server` 别名保留是为了让下方 // 大段表单代码改动最小。 const { - secrets: server, + secrets: rawServer, secretsError, setSecrets: setServer, catalog, @@ -251,6 +272,10 @@ export default function SettingsPage() { downloadBusy, startDownload, } = useSettingsData() + const server = useMemo( + () => (rawServer ? withSecretsDefaults(rawServer) : null), + [rawServer], + ) const [draft, setDraft] = useState(EMPTY) const [error, setError] = useState(null) const [saving, setSaving] = useState(false) @@ -2189,4 +2214,3 @@ function XformersSection() { ) } -