chore: sync production runtime and docs
This commit is contained in:
+50
-28
@@ -304,11 +304,16 @@ function App() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const frameId = requestAnimationFrame(() => {
|
||||
const invalidateMap = () => {
|
||||
mapRef.current?.invalidateSize(false);
|
||||
});
|
||||
};
|
||||
const frameId = requestAnimationFrame(invalidateMap);
|
||||
const timeoutId = window.setTimeout(invalidateMap, 120);
|
||||
|
||||
return () => cancelAnimationFrame(frameId);
|
||||
return () => {
|
||||
cancelAnimationFrame(frameId);
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [isStandaloneLeftPage, leftPanelWidth, rightPanelWidth]);
|
||||
|
||||
const getVisibleLayerRefs = useCallback(() => ({
|
||||
@@ -1624,32 +1629,49 @@ function App() {
|
||||
psPanel={psPanel}
|
||||
/>
|
||||
|
||||
{!isStandaloneLeftPage && (
|
||||
<>
|
||||
<div className="panel-resizer" onMouseDown={(event) => startResize('left', event)} />
|
||||
<div
|
||||
className="panel-resizer"
|
||||
onMouseDown={(event) => startResize('left', event)}
|
||||
style={{ display: isStandaloneLeftPage ? 'none' : undefined }}
|
||||
/>
|
||||
|
||||
<AppMapWorkspace
|
||||
language={language}
|
||||
showMapRegionLocator={showMapRegionLocator}
|
||||
toggleMapRegionLocator={toggleMapRegionLocator}
|
||||
mapRegionOptions={mapRegionOptions}
|
||||
mapRegionSelection={mapRegionSelection}
|
||||
mapRegionLoading={mapRegionLoading}
|
||||
mapRegionLocating={mapRegionLocating}
|
||||
mapRegionError={mapRegionError}
|
||||
mapRegionLocatedName={mapRegionLocatedName}
|
||||
onMapRegionProvinceChange={handleMapRegionProvinceChange}
|
||||
onMapRegionCityChange={handleMapRegionCityChange}
|
||||
onLocateSelectedRegion={locateSelectedRegionOnMap}
|
||||
onClearMapRegionHighlight={clearMapRegionHighlight}
|
||||
baseLayerKey={baseLayerKey}
|
||||
setBaseLayerKey={setBaseLayerKey}
|
||||
onOpenExportModal={mapExport.openExportModal}
|
||||
/>
|
||||
<div className="panel-resizer" onMouseDown={(event) => startResize('right', event)} />
|
||||
<AppLogPanel width={rightPanelWidth} />
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: isStandaloneLeftPage ? 'none' : 'flex',
|
||||
flex: '1 1 auto',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
<AppMapWorkspace
|
||||
language={language}
|
||||
showMapRegionLocator={showMapRegionLocator}
|
||||
toggleMapRegionLocator={toggleMapRegionLocator}
|
||||
mapRegionOptions={mapRegionOptions}
|
||||
mapRegionSelection={mapRegionSelection}
|
||||
mapRegionLoading={mapRegionLoading}
|
||||
mapRegionLocating={mapRegionLocating}
|
||||
mapRegionError={mapRegionError}
|
||||
mapRegionLocatedName={mapRegionLocatedName}
|
||||
onMapRegionProvinceChange={handleMapRegionProvinceChange}
|
||||
onMapRegionCityChange={handleMapRegionCityChange}
|
||||
onLocateSelectedRegion={locateSelectedRegionOnMap}
|
||||
onClearMapRegionHighlight={clearMapRegionHighlight}
|
||||
baseLayerKey={baseLayerKey}
|
||||
setBaseLayerKey={setBaseLayerKey}
|
||||
onOpenExportModal={mapExport.openExportModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="panel-resizer"
|
||||
onMouseDown={(event) => startResize('right', event)}
|
||||
style={{ display: isStandaloneLeftPage ? 'none' : undefined }}
|
||||
/>
|
||||
|
||||
<div style={{ display: isStandaloneLeftPage ? 'none' : undefined }}>
|
||||
<AppLogPanel width={rightPanelWidth} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppOverlays
|
||||
|
||||
@@ -74,6 +74,24 @@ const PYINT_PRECISE_ORBIT_MODE_LABEL = {
|
||||
replace_and_validate: '替换并校验',
|
||||
};
|
||||
|
||||
const RERUN_MODE_LABEL = {
|
||||
unfinished_only: '只跑未完成',
|
||||
rerun_all: '全部重跑',
|
||||
};
|
||||
|
||||
const RERUN_MODE_OPTIONS = [
|
||||
{
|
||||
value: 'unfinished_only',
|
||||
label: '只跑未完成',
|
||||
description: '按当前引擎和当前模板检查已有结果,已完成任务会跳过。',
|
||||
},
|
||||
{
|
||||
value: 'rerun_all',
|
||||
label: '全部重跑',
|
||||
description: '忽略已有结果,对本次选中的任务全部重新执行。',
|
||||
},
|
||||
];
|
||||
|
||||
function formatEngineLabel(engineCode, engineLabel = '') {
|
||||
return engineLabel || ENGINE_LABEL[engineCode] || engineCode || '-';
|
||||
}
|
||||
@@ -312,6 +330,8 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitMsg, setSubmitMsg] = useState('');
|
||||
const [submitError, setSubmitError] = useState(false);
|
||||
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
|
||||
const [rerunMode, setRerunMode] = useState('unfinished_only');
|
||||
const [pyintPreview, setPyintPreview] = useState(null);
|
||||
const [pyintPreviewLoading, setPyintPreviewLoading] = useState(false);
|
||||
const [pyintPreviewFeedback, setPyintPreviewFeedback] = useState({ message: '', error: false });
|
||||
@@ -523,7 +543,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
}
|
||||
}, [numToProcess, rootDir]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleOpenSubmitDialog = useCallback(() => {
|
||||
if (!rootDir.trim()) {
|
||||
setSubmitError(true);
|
||||
setSubmitMsg('请输入根目录。');
|
||||
@@ -534,23 +554,37 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
setSubmitMsg('PyINT 输入资产预检未通过,请先修复阻塞项。');
|
||||
return;
|
||||
}
|
||||
setSubmitError(false);
|
||||
setSubmitMsg('');
|
||||
setSubmitDialogOpen(true);
|
||||
}, [pyintPreviewBlocksSubmit, rootDir]);
|
||||
|
||||
const handleCloseSubmitDialog = useCallback(() => {
|
||||
if (submitting) return;
|
||||
setSubmitDialogOpen(false);
|
||||
}, [submitting]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSubmitting(true);
|
||||
setSubmitMsg('');
|
||||
setSubmitError(false);
|
||||
try {
|
||||
const extra = buildExtraPayload(currentParamSchema, engineExtraParams);
|
||||
setSubmitDialogOpen(false);
|
||||
const result = await submitRun({
|
||||
engine_code: selectedEngine,
|
||||
profile: selectedProfile,
|
||||
root_dir: rootDir.trim(),
|
||||
num_to_process: Number(numToProcess) || 0,
|
||||
rerun_mode: rerunMode,
|
||||
timeout_seconds: timeoutSec ? Number(timeoutSec) : null,
|
||||
extra,
|
||||
});
|
||||
const taskCount = result?.selected_task_count ? `,选中 ${result.selected_task_count} 个任务` : '';
|
||||
const skippedCompleted = Number(result?.skipped_completed_count || 0);
|
||||
const skippedText = skippedCompleted > 0 ? `,跳过 ${skippedCompleted} 个已完成任务` : '';
|
||||
setSubmitError(false);
|
||||
setSubmitMsg(`任务已入队:${result.task_id}${taskCount}`);
|
||||
setSubmitMsg(`任务已入队:${result.task_id}${taskCount}${skippedText}`);
|
||||
if (onJobQueued) onJobQueued(result.task_id);
|
||||
await refreshMonitor();
|
||||
} catch (err) {
|
||||
@@ -625,6 +659,135 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
</div>
|
||||
)}
|
||||
|
||||
{submitDialogOpen && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(15,23,42,0.42)',
|
||||
zIndex: 9998,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 'min(560px, 100%)',
|
||||
background: '#fff',
|
||||
borderRadius: 14,
|
||||
border: '1px solid #cbd5e1',
|
||||
boxShadow: '0 20px 60px rgba(15, 23, 42, 0.24)',
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, marginBottom: 10 }}>
|
||||
<strong style={{ fontSize: 16, color: '#0f172a' }}>提交生产任务</strong>
|
||||
<button
|
||||
onClick={handleCloseSubmitDialog}
|
||||
disabled={submitting}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
color: '#64748b',
|
||||
cursor: submitting ? 'not-allowed' : 'pointer',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, color: '#475569', lineHeight: 1.6, marginBottom: 12 }}>
|
||||
选择本次批处理策略。任务数量限制会在“只跑未完成”过滤之后再生效,按当前引擎和当前模板判断已完成状态。
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 14 }}>
|
||||
{RERUN_MODE_OPTIONS.map(option => {
|
||||
const selected = rerunMode === option.value;
|
||||
return (
|
||||
<label
|
||||
key={option.value}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'flex-start',
|
||||
padding: 12,
|
||||
borderRadius: 10,
|
||||
border: selected ? '2px solid #3b82f6' : '1px solid #dbeafe',
|
||||
background: selected ? '#eff6ff' : '#f8fafc',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="rerun-mode"
|
||||
checked={selected}
|
||||
onChange={() => setRerunMode(option.value)}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', marginBottom: 4 }}>{option.label}</div>
|
||||
<div style={{ fontSize: 12, color: '#64748b', lineHeight: 1.5 }}>{option.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 10,
|
||||
border: '1px solid #e2e8f0',
|
||||
background: '#f8fafc',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 12, color: '#475569' }}>引擎:{formatEngineLabel(selectedEngine, currentEngineObj?.engine_label)}</div>
|
||||
<div style={{ fontSize: 12, color: '#475569' }}>模板:{currentProfileObj?.label || selectedProfile}</div>
|
||||
<div style={{ fontSize: 12, color: '#475569' }}>任务数量:{Number(numToProcess) > 0 ? Number(numToProcess) : '全部'}</div>
|
||||
<div style={{ fontSize: 12, color: '#475569' }}>执行策略:{RERUN_MODE_LABEL[rerunMode] || rerunMode}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
|
||||
<button
|
||||
onClick={handleCloseSubmitDialog}
|
||||
disabled={submitting}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #cbd5e1',
|
||||
background: '#fff',
|
||||
color: '#0f172a',
|
||||
cursor: submitting ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
borderRadius: 6,
|
||||
border: 'none',
|
||||
background: '#2563eb',
|
||||
color: '#fff',
|
||||
cursor: submitting ? 'not-allowed' : 'pointer',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{submitting ? '提交中...' : `确认 ${RERUN_MODE_LABEL[rerunMode] || ''}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={card}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<strong style={{ fontSize: 14 }}>引擎状态</strong>
|
||||
@@ -967,7 +1130,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
onClick={handleOpenSubmitDialog}
|
||||
disabled={isSubmitDisabled}
|
||||
style={{
|
||||
padding: '6px 20px',
|
||||
|
||||
@@ -335,9 +335,15 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
const databaseStatus = status?.database || {};
|
||||
const dinsarBridge = asObject(status?.dinsar_bridge);
|
||||
const dinsarResultCatalog = asObject(status?.dinsar_result_catalog || status?.result_catalog);
|
||||
const timeseriesResultCatalog = asObject(status?.timeseries_result_catalog || status?.psinsar_result_catalog);
|
||||
const dinsarCatalogNeedsRebuild =
|
||||
typeof dinsarResultCatalog.needs_rebuild === 'boolean' ? dinsarResultCatalog.needs_rebuild : null;
|
||||
const timeseriesCatalogNeedsRebuild =
|
||||
typeof timeseriesResultCatalog.needs_rebuild === 'boolean' ? timeseriesResultCatalog.needs_rebuild : null;
|
||||
const sourceRoots = asObject(status?.source_roots);
|
||||
const productPackages = asObject(status?.product_packages);
|
||||
const wslRuntime = asObject(status?.wsl_runtime);
|
||||
const wslRuntimeItems = asArray(wslRuntime.runtimes);
|
||||
const pairingSystem = asObject(status?.pairing_system);
|
||||
const sourceRootItems = asArray(sourceRoots.items);
|
||||
const bridgeDiagnosisIssueCount =
|
||||
@@ -616,6 +622,41 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="health-card">
|
||||
<div className="health-card-title">{en ? 'Timeseries Result Catalog' : '时序 InSAR 结果目录'}</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Catalog status' : '目录状态'}</span>
|
||||
{renderBadge(
|
||||
isCatalogHealthy(timeseriesResultCatalog),
|
||||
formatCatalogStatusLabel(timeseriesResultCatalog, en)
|
||||
)}
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Needs rebuild' : '需要重建'}</span>
|
||||
{renderBadge(
|
||||
timeseriesCatalogNeedsRebuild === false,
|
||||
timeseriesCatalogNeedsRebuild === null
|
||||
? (en ? 'Unknown' : '未知')
|
||||
: (timeseriesCatalogNeedsRebuild ? (en ? 'Yes' : '是') : (en ? 'No' : '否'))
|
||||
)}
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Manifest / DB' : 'Manifest / 数据库'}</span>
|
||||
<span>
|
||||
{toNumber(timeseriesResultCatalog.manifest_count)} / {toNumber(timeseriesResultCatalog.db_count)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Issue count' : '问题数量'}</span>
|
||||
<span>{toNumber(timeseriesResultCatalog.issue_count)}</span>
|
||||
</div>
|
||||
<div className="health-card-note">
|
||||
{en
|
||||
? 'Managed timeseries products are indexed from canonical publish manifests.'
|
||||
: '时序 InSAR 产物已按标准发布包 manifest 进行索引和自检。'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="health-card">
|
||||
<div className="health-card-title">{en ? 'Pairing System' : '配对系统'}</div>
|
||||
<div className="health-card-row">
|
||||
@@ -810,6 +851,78 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="health-card">
|
||||
<div className="health-card-title">{en ? 'Product Packages' : '标准结果包'}</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Overall' : '总体状态'}</span>
|
||||
{renderBadge(
|
||||
productPackages.ok,
|
||||
`${toNumber(productPackages.canonical_count)} / ${toNumber(productPackages.total_count)}`
|
||||
)}
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Manifest / publish dir' : 'Manifest / 发布目录'}</span>
|
||||
<span>
|
||||
{toNumber(productPackages.missing_manifest_count)} / {toNumber(productPackages.missing_publish_dir_count)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Processor / runtime' : '处理器 / 运行时'}</span>
|
||||
<span>
|
||||
{toNumber(productPackages.missing_processor_count)} / {toNumber(productPackages.missing_runtime_count)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Native output trace' : '原生输出追踪'}</span>
|
||||
<span>{toNumber(productPackages.missing_native_output_count)}</span>
|
||||
</div>
|
||||
<div className="health-card-note">
|
||||
{en
|
||||
? 'Catalog registration now checks canonical package metadata instead of guessing engine-native directories.'
|
||||
: '目录登记现在直接校验标准包元数据,不再依赖猜测引擎原生目录结构。'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="health-card">
|
||||
<div className="health-card-title">{en ? 'WSL Runtime' : 'WSL 运行时'}</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Overall' : '总体状态'}</span>
|
||||
{renderBadge(
|
||||
wslRuntime.ok,
|
||||
`${toNumber(wslRuntime.healthy_runtime_count)} / ${toNumber(wslRuntime.required_runtime_count)}`
|
||||
)}
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Broker root' : 'Broker 根目录'}</span>
|
||||
{renderBadge(wslRuntime.broker_job_root_exists)}
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Shared distro' : '共享发行版'}</span>
|
||||
<span>{wslRuntime.shared_distro || '-'}</span>
|
||||
</div>
|
||||
<div className="health-card-row">
|
||||
<span>{en ? 'Shared conda env' : '共享 conda 环境'}</span>
|
||||
<span>{wslRuntime.shared_conda_env_name || '-'}</span>
|
||||
</div>
|
||||
{wslRuntimeItems.length > 0 && (
|
||||
wslRuntimeItems.map((item) => (
|
||||
<div
|
||||
key={item.runtime_id || item.engine_code}
|
||||
className={`health-card-note ${item.ok ? 'ok' : (item.required ? 'error' : 'warn')}`}
|
||||
>
|
||||
<div style={{ fontWeight: 600 }}>
|
||||
{item.display_name || item.runtime_id || item.engine_code}
|
||||
</div>
|
||||
<div>
|
||||
{(item.required ? (en ? 'Required' : '必需') : (en ? 'Reserved' : '预留'))}
|
||||
{' · '}
|
||||
{item.runner_exists ? (en ? 'Runner OK' : 'Runner 正常') : (en ? 'Runner missing' : 'Runner 缺失')}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="health-card">
|
||||
<div className="health-card-title">IDL/ENVI</div>
|
||||
<div className="health-card-row">
|
||||
|
||||
Reference in New Issue
Block a user