From 2d65bc7ce9d6466d094300874e63030baedafe33 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 3 May 2026 16:49:00 +0800 Subject: [PATCH] Improve DInSAR production frontend monitoring and catalog layout --- frontend/src/App.css | 24 ++++ frontend/src/DinsarProductionPanel.jsx | 179 ++++++++++++++++++++++--- 2 files changed, 181 insertions(+), 22 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 465fed4..e7ff02b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -4366,6 +4366,7 @@ input[type="checkbox"] { grid-template-columns: minmax(360px, 420px) minmax(0, 1fr); gap: 14px; min-height: 0; + align-items: start; } .panel--standalone .dinsar-catalog-workspace { @@ -4383,6 +4384,16 @@ input[type="checkbox"] { overflow: hidden; } +.dinsar-catalog-list-card { + position: sticky; + top: 16px; + max-height: min(760px, calc(100vh - 140px)); +} + +.panel--standalone .dinsar-catalog-list-card { + max-height: min(820px, calc(100vh - 112px)); +} + .panel--standalone { width: 100%; border-right: none; @@ -4528,7 +4539,11 @@ input[type="checkbox"] { display: grid; gap: 10px; padding: 12px; + min-height: 0; + flex: 1 1 auto; overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; } .dinsar-catalog-list-item { @@ -4979,6 +4994,15 @@ input[type="checkbox"] { .panel-standalone-actions { justify-content: flex-start; } + + .dinsar-catalog-list-card { + position: static; + max-height: none; + } + + .dinsar-catalog-list { + max-height: 48vh; + } } @media (max-width: 640px) { diff --git a/frontend/src/DinsarProductionPanel.jsx b/frontend/src/DinsarProductionPanel.jsx index ed9e2fb..b9136c1 100644 --- a/frontend/src/DinsarProductionPanel.jsx +++ b/frontend/src/DinsarProductionPanel.jsx @@ -2,7 +2,7 @@ import { listEngines, listRuns, previewPyintInputAssets, submitRun } from './api/dinsarProduction'; import { getJobLog } from './api/idl'; -import { clearTaskLogs, deleteTaskLog, getActiveTasks, getTaskLogs } from './api/tasks'; +import { clearTaskLogs, deleteTaskLog, getActiveTasks, getRecentTasks, getTaskLogs } from './api/tasks'; const card = { background: '#fff', @@ -40,7 +40,7 @@ const ENGINE_LABEL = { const TASK_TYPE_LABEL = { ISCE2_RUN: 'ISCE2生产', - PYINT_RUN: 'PyINT生产', + PYINT_RUN: 'PyINT/Gamma生产', IDL_RUN_DINSAR: 'ENVI生产', }; @@ -104,6 +104,96 @@ function formatStatus(status) { return STATUS_LABEL[status] || status || '-'; } +function taskTypeToEngine(taskType) { + if (taskType === 'ISCE2_RUN') return 'isce2'; + if (taskType === 'PYINT_RUN') return 'pyint'; + if (taskType === 'IDL_RUN_DINSAR') return 'sarscape'; + return ''; +} + +function taskStatusToRunStatus(status) { + const normalized = String(status || '').toUpperCase(); + if (normalized === 'COMPLETED') return 'success'; + if (normalized === 'FAILED') return 'failed'; + if (normalized === 'CANCELLED' || normalized === 'CANCELED') return 'cancelled'; + if (normalized === 'RUNNING') return 'running'; + return 'pending'; +} + +function epochSeconds(value) { + if (value == null || value === '') return null; + if (typeof value === 'number') return value; + const parsed = Date.parse(value); + return Number.isNaN(parsed) ? null : Math.floor(parsed / 1000); +} + +function taskToRunRow(task) { + const taskId = task?.task_id || ''; + return { + run_id: taskId, + record_type: 'task', + log_source: 'task', + product_family: 'dinsar', + engine: taskTypeToEngine(task?.task_type), + profile_code: '', + status: taskStatusToRunStatus(task?.status), + raw_status: task?.status, + started_at: epochSeconds(task?.started_at || task?.created_at), + ended_at: epochSeconds(task?.ended_at), + task_id: taskId, + task_type: task?.task_type, + workflow_run_id: '', + root_dir: '', + publish_root_dir: '', + message: task?.message || task?.task_name || '', + total_items: null, + completed_items: null, + failed_items: null, + skipped_items: null, + }; +} + +function mergeRunRows(productionRuns, recentTasks, limit = 20) { + const rows = (productionRuns || []).map(run => ({ + ...run, + record_type: run?.record_type || 'run', + log_source: run?.log_source || 'run', + })); + const representedTaskIds = new Set( + rows + .map(run => String(run?.task_id || '').trim()) + .filter(Boolean), + ); + + (recentTasks || []).forEach(task => { + if (!['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'].includes(task?.task_type)) { + return; + } + const taskId = String(task?.task_id || '').trim(); + if (!taskId || representedTaskIds.has(taskId)) { + return; + } + rows.push(taskToRunRow(task)); + representedTaskIds.add(taskId); + }); + + return rows + .sort((a, b) => Number(b?.started_at || 0) - Number(a?.started_at || 0)) + .slice(0, limit); +} + +function formatTaskLogContent(taskId, logs) { + if (!Array.isArray(logs) || logs.length === 0) { + return '暂无任务日志。'; + } + const body = logs.map(log => { + const timestamp = String(log.timestamp || '').replace('T', ' ').replace('Z', ''); + const level = log.level || 'INFO'; + return `${timestamp} [${level}] ${log.message || ''}`; + }).join('\n'); + return `task_id=${taskId}\n${body}`; +} + function formatPyintDemMode(mode) { return PYINT_DEM_MODE_LABEL[mode] || mode || '-'; } @@ -390,6 +480,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) const [logModal, setLogModal] = useState({ open: false, runId: '', content: '', loading: false }); const [activeTask, setActiveTask] = useState(null); + const [recentTask, setRecentTask] = useState(null); const [taskLogs, setTaskLogs] = useState([]); const [taskLogsLoading, setTaskLogsLoading] = useState(false); const [taskLogActionLoading, setTaskLogActionLoading] = useState(false); @@ -402,13 +493,13 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) const currentParamSections = buildParamSections(currentParamSchema); const currentDefaultTimeoutSec = Number(currentEngineObj?.default_timeout_seconds || 0) || 0; const currentParamHelpText = selectedEngine === 'pyint' - ? '这些参数影响 PyINT 的多视、并行度以及是否执行解缠/地理编码。建议先直接使用默认值,优先确认当前任务目录里的 LT-1 原始压缩包是否能被正常识别。' + ? 'PyINT/Gamma 默认按目标网格尺寸自动换算多视,逻辑与 ENVI/SARscape 自定义流程一致;通常只需要设置目标网格、并行度和是否执行解缠/地理编码。' : selectedEngine === 'isce2' ? '这些参数现在按执行、交付、增强分组展示。结果异常时,优先尝试关闭增强项,再回看基础几何和配对质量。' : '这些参数影响当前引擎的生产模板。建议先使用默认值,只有在结果边界、噪声或几何表现异常时再逐项调整。'; const pyintPreviewBlocksSubmit = selectedEngine === 'pyint' && pyintPreview && pyintPreview.allow_submit === false; const latestRunWithTask = runs.find(run => run?.task_id) || null; - const monitoredTask = activeTask || ( + const monitoredTask = activeTask || recentTask || ( latestRunWithTask ? { task_id: latestRunWithTask.task_id, @@ -439,18 +530,23 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) } }, []); - const loadRuns = useCallback(async () => { - setRunsLoading(true); + const loadRuns = useCallback(async (options = {}) => { + const silent = !!options.silent; + if (!silent) setRunsLoading(true); try { - const data = await listRuns(20); - const nextRuns = data.runs || []; + const [runData, taskData] = await Promise.all([ + listRuns(20), + getRecentTasks(['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'], [], 20, 0), + ]); + const recentTasks = Array.isArray(taskData) ? taskData : (taskData?.tasks || []); + const nextRuns = mergeRunRows(runData.runs || [], recentTasks, 20); setRuns(nextRuns); return nextRuns; } catch { setRuns([]); return []; } finally { - setRunsLoading(false); + if (!silent) setRunsLoading(false); } }, []); @@ -467,19 +563,33 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) } }, []); - const loadTaskLogs = useCallback(async taskId => { + const loadRecentTask = useCallback(async () => { + try { + const data = await getRecentTasks(['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'], [], 1, 0); + const tasks = Array.isArray(data) ? data : (data?.tasks || []); + const relevantTask = tasks[0] || null; + setRecentTask(relevantTask); + return relevantTask; + } catch { + setRecentTask(null); + return null; + } + }, []); + + const loadTaskLogs = useCallback(async (taskId, options = {}) => { + const silent = !!options.silent; if (!taskId) { setTaskLogs([]); return; } - setTaskLogsLoading(true); + if (!silent) setTaskLogsLoading(true); try { - const data = await getTaskLogs(taskId, 50, 0); + const data = await getTaskLogs(taskId, 120, 0); setTaskLogs(data?.logs || []); } catch { setTaskLogs([]); } finally { - setTaskLogsLoading(false); + if (!silent) setTaskLogsLoading(false); } }, []); @@ -519,20 +629,34 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) } }, [logTaskId, loadTaskLogs, taskLogActionLoading, taskLogs.length]); - const refreshMonitor = useCallback(async () => { - const [nextRuns, nextActiveTask] = await Promise.all([ - loadRuns(), + const refreshMonitor = useCallback(async (options = {}) => { + const silent = !!options.silent; + const [nextRuns, nextActiveTask, nextRecentTask] = await Promise.all([ + loadRuns({ silent }), loadActiveTask(), + loadRecentTask(), ]); - const fallbackTaskId = nextActiveTask?.task_id || nextRuns.find(run => run?.task_id)?.task_id || ''; - await loadTaskLogs(fallbackTaskId); - }, [loadActiveTask, loadRuns, loadTaskLogs]); + const fallbackTaskId = + nextActiveTask?.task_id + || nextRecentTask?.task_id + || nextRuns.find(run => run?.task_id)?.task_id + || ''; + await loadTaskLogs(fallbackTaskId, { silent }); + }, [loadActiveTask, loadRecentTask, loadRuns, loadTaskLogs]); useEffect(() => { loadEngines(); refreshMonitor(); }, [loadEngines, refreshMonitor]); + useEffect(() => { + const intervalMs = activeTask ? 5000 : 15000; + const timer = window.setInterval(() => { + refreshMonitor({ silent: true }); + }, intervalMs); + return () => window.clearInterval(timer); + }, [activeTask, refreshMonitor]); + useEffect(() => { if (currentProfiles.length > 0) { setSelectedProfile(currentProfiles[0].code); @@ -647,9 +771,20 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) } }; - const handleViewLog = async runId => { + const handleViewLog = async run => { + const runId = typeof run === 'string' ? run : (run?.run_id || run?.task_id || ''); setLogModal({ open: true, runId, content: '', loading: true }); try { + if (typeof run !== 'string' && run?.log_source === 'task' && run?.task_id) { + const data = await getTaskLogs(run.task_id, 200, 0); + setLogModal({ + open: true, + runId, + content: formatTaskLogContent(run.task_id, data?.logs || []), + loading: false, + }); + return; + } const data = await getJobLog(runId); setLogModal({ open: true, runId, content: data.content || '', loading: false }); } catch { @@ -1389,7 +1524,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued }) {runs.map(run => ( - + {run.run_id} {formatEngineLabel(run.engine)}