docs: update production workflow design and runtime changes

This commit is contained in:
2026-06-14 18:47:00 +08:00
parent 58a87706ef
commit b1c59051b9
41 changed files with 2042 additions and 134 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:8900 http://127.0.0.1:8910 https://*.tile.openstreetmap.org https://*.tile.opentopomap.org; connect-src 'self' http://127.0.0.1:8900 http://127.0.0.1:8910; font-src 'self' data:; worker-src blob:;" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.tile.openstreetmap.org https://*.tile.opentopomap.org; connect-src 'self'; font-src 'self' data:; worker-src blob:;" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
+28 -19
View File
@@ -9,8 +9,10 @@ const DEFAULT_MONITOR_CONFIG = {
dinsar_dirs: [],
gf3_archive_source_dirs: [],
gf3_source_dirs: [],
gf3_legacy_gdal_enabled: false,
gf3_sarscape_native_dirs: [],
gf3_storage_dirs: [],
gf3_sarscape_runtime_dir: '',
gf3_sarscape_wrapper_exe: '',
gf3_sarscape_idlrt_path: '',
gf3_sarscape_dem_path: '',
@@ -302,6 +304,7 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
const hasGf3SourceDirs = config.gf3_source_dirs.length > 0;
const hasGf3SarscapeNativeDirs = config.gf3_sarscape_native_dirs.length > 0;
const hasGf3StorageDirs = config.gf3_storage_dirs.length > 0;
const gf3LegacyGdalEnabled = Boolean(config.gf3_legacy_gdal_enabled);
const hasGf3SarscapeWrapper = typeof config.gf3_sarscape_wrapper_exe === 'string' && config.gf3_sarscape_wrapper_exe.trim() !== '';
const hasGf3SarscapeDem = typeof config.gf3_sarscape_dem_path === 'string' && config.gf3_sarscape_dem_path.trim() !== '';
@@ -311,8 +314,8 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
const canRunS1Scan = !readOnly && configLoaded && hasS1SourceDirs;
const canRunS1OrbitScan = !readOnly && configLoaded && hasS1OrbitDirs;
const canRunGf3Scan = !readOnly && configLoaded && hasGf3StorageDirs;
const canRunGf3Unpack = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SourceDirs;
const canRunGf3Process = !readOnly && configLoaded && hasGf3SourceDirs;
const canRunGf3Unpack = !readOnly && configLoaded && gf3LegacyGdalEnabled && hasGf3ArchiveSourceDirs && hasGf3SourceDirs;
const canRunGf3Process = !readOnly && configLoaded && gf3LegacyGdalEnabled && hasGf3SourceDirs;
const canRunGf3SarscapeProduce = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SarscapeNativeDirs && hasGf3StorageDirs && hasGf3SarscapeWrapper && hasGf3SarscapeDem;
const canRunGf3SarscapeSync = !readOnly && configLoaded && hasGf3SarscapeNativeDirs && hasGf3StorageDirs;
const canRunGf3SarscapeClean = !readOnly && configLoaded && hasGf3SarscapeNativeDirs && hasGf3StorageDirs;
@@ -797,9 +800,10 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
<div style={rowStyle}><span style={labelStyle}>S1 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_storage_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>S1 精轨</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_orbit_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>GF3 压缩包</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_archive_source_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>GF3 来源</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_source_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>GF3 legacy L1A</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_source_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>GF3 原生</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_sarscape_native_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>GF3 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_storage_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>GF3 runtime</span><span style={{ wordBreak: 'break-all' }}>{config.gf3_sarscape_runtime_dir || '未配置'}</span></div>
<div style={rowStyle}><span style={labelStyle}>D-InSAR 结果</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.dinsar_dirs)}</span></div>
</div>
</div>
@@ -888,31 +892,36 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
</div>
<div style={sectionStyle}>
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>GF3 归档预处理</div>
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>GF3 SARscape 生产</div>
<div style={{ ...gridStyle, marginBottom: '8px' }}>
<div style={rowStyle}><span style={labelStyle}>压缩包来源</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_archive_source_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>L1A 来源</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_source_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>SARscape 原生</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_sarscape_native_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>L2 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_storage_dirs)}</span></div>
<div style={rowStyle}><span style={labelStyle}>Runtime</span><span style={{ wordBreak: 'break-all' }}>{config.gf3_sarscape_runtime_dir || '未配置'}</span></div>
<div style={rowStyle}><span style={labelStyle}>Wrapper</span><span style={{ wordBreak: 'break-all' }}>{config.gf3_sarscape_wrapper_exe || '未配置'}</span></div>
<div style={rowStyle}><span style={labelStyle}>SARscape DEM</span><span style={{ wordBreak: 'break-all' }}>{config.gf3_sarscape_dem_path || '未配置'}</span></div>
<div style={rowStyle}><span style={labelStyle}>极化</span><span>{config.gf3_sarscape_polarizations || 'HH,HV'}</span></div>
<div style={rowStyle}><span style={labelStyle}>Legacy GDAL</span><span>{gf3LegacyGdalEnabled ? '启用' : '关闭'}</span></div>
</div>
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<button
onClick={handleGf3Unpack}
disabled={gf3UnpackLoading || readOnly || !canRunGf3Unpack}
style={actionBtnStyle(gf3UnpackLoading, readOnly || !canRunGf3Unpack)}
>
{gf3UnpackLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 解包')}
</button>
<button
onClick={handleGf3BatchProcess}
disabled={gf3ProcessLoading || readOnly || !canRunGf3Process}
style={actionBtnStyle(gf3ProcessLoading, readOnly || !canRunGf3Process)}
>
{gf3ProcessLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 预处理')}
</button>
{gf3LegacyGdalEnabled && (
<>
<button
onClick={handleGf3Unpack}
disabled={gf3UnpackLoading || readOnly || !canRunGf3Unpack}
style={actionBtnStyle(gf3UnpackLoading, readOnly || !canRunGf3Unpack)}
>
{gf3UnpackLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 legacy 解包')}
</button>
<button
onClick={handleGf3BatchProcess}
disabled={gf3ProcessLoading || readOnly || !canRunGf3Process}
style={actionBtnStyle(gf3ProcessLoading, readOnly || !canRunGf3Process)}
>
{gf3ProcessLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 legacy 预处理')}
</button>
</>
)}
<button
onClick={handleGf3SarscapeProduce}
disabled={gf3SarscapeProduceLoading || readOnly || !canRunGf3SarscapeProduce}
+5 -2
View File
@@ -38,8 +38,11 @@ export const TILE_LAYER_OPTIONS = {
maxZoom: 16,
maxNativeZoom: 16,
tms: false,
updateWhenIdle: true,
keepBuffer: 4,
updateWhenIdle: false,
updateWhenZooming: false,
keepBuffer: 2,
reuseTiles: true,
crossOrigin: 'anonymous',
};
export const TILE_LAYER_BOUNDS = [
+2 -3
View File
@@ -10,8 +10,8 @@ const TASK_UI_POLICIES = {
SCAN_HAZARD: { label: '灾害点同步', featureScope: 'hazard' },
UNPACK_ARCHIVES: { label: 'LT-1 解包', featureScope: 'data_monitor' },
UNPACK_SENTINEL1: { label: 'Sentinel-1 解包', featureScope: 'data_monitor' },
GF3_UNPACK: { label: 'GF3 解包', featureScope: 'data_monitor' },
GF3_BATCH_PROCESS: { label: 'GF3 预处理', featureScope: 'data_monitor' },
GF3_UNPACK: { label: 'GF3 legacy 解包', featureScope: 'data_monitor' },
GF3_BATCH_PROCESS: { label: 'GF3 legacy 预处理', featureScope: 'data_monitor' },
GF3_SARSCAPE_PRODUCE: { label: 'GF3 SARscape 生产', featureScope: 'data_monitor' },
GF3_SARSCAPE_SYNC: { label: 'GF3 SARscape 入库', featureScope: 'data_monitor' },
GF3_SARSCAPE_CLEAN: { label: 'GF3 中间清理', featureScope: 'data_monitor' },
@@ -73,4 +73,3 @@ export function isTaskGloballyBlocking(taskType) {
export function getTaskTypeLabel(taskType) {
return getTaskUiPolicy(taskType).label;
}
+50 -8
View File
@@ -3,6 +3,39 @@ import apiClient from '../api/client';
import { getHealth } from '../api/health';
const HEALTH_POLL_INTERVAL_MS = 30000;
const STARTUP_RETRY_DELAYS_MS = [750, 1500, 3000];
const sleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
const getErrorStatus = (error) => Number(error?.response?.status || 0);
const isTransientApiError = (error) => {
const status = getErrorStatus(error);
return !status || status === 408 || status === 429 || status >= 500;
};
const requestWithTransientRetry = async (
requestFn,
{
delays = STARTUP_RETRY_DELAYS_MS,
shouldRetry = isTransientApiError,
} = {},
) => {
let lastError = null;
const retryDelays = Array.isArray(delays) ? delays : [];
for (let attempt = 0; attempt <= retryDelays.length; attempt += 1) {
try {
return await requestFn();
} catch (error) {
lastError = error;
if (attempt >= retryDelays.length || !shouldRetry(error)) {
throw error;
}
await sleep(retryDelays[attempt]);
}
}
throw lastError;
};
export default function useAppAuthLifecycle({
ensureCanOperate,
@@ -33,15 +66,21 @@ export default function useAppAuthLifecycle({
const { clearOnFailure = true } = options;
const requestGeneration = authGenerationRef.current;
try {
const response = await apiClient.get('/auth/me', {
skipAuthReset: !clearOnFailure,
authGeneration: requestGeneration,
});
const response = await requestWithTransientRetry(() => (
apiClient.get('/auth/me', {
skipAuthReset: !clearOnFailure,
authGeneration: requestGeneration,
})
));
if (requestGeneration === authGenerationRef.current) {
setCurrentUser(response.data || null);
}
} catch {
if (clearOnFailure && requestGeneration === authGenerationRef.current) {
} catch (error) {
if (
clearOnFailure
&& getErrorStatus(error) === 401
&& requestGeneration === authGenerationRef.current
) {
setCurrentUser(null);
}
} finally {
@@ -88,7 +127,7 @@ export default function useAppAuthLifecycle({
const fetchLicenseStatus = useCallback(async () => {
try {
setLicenseLoading(true);
const response = await apiClient.get('/license/status');
const response = await requestWithTransientRetry(() => apiClient.get('/license/status'));
const data = response.data || {};
if (!data.ok && !data.reason) {
data.reason = '未授权';
@@ -111,7 +150,10 @@ export default function useAppAuthLifecycle({
setHealthLoading(true);
}
setHealthError('');
const data = await getHealth(refresh ? { refresh: true } : {});
const data = await requestWithTransientRetry(
() => getHealth(refresh ? { refresh: true } : {}),
{ delays: silent ? [] : STARTUP_RETRY_DELAYS_MS.slice(0, 2) },
);
setHealthStatus(data || null);
} catch (error) {
setHealthError(error.response?.data?.detail || '运维自检失败');