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
+46 -16
View File
@@ -43,6 +43,10 @@ CONDA_ENV_NAME=
NGINX_PATH=C:\nginx\nginx.exe NGINX_PATH=C:\nginx\nginx.exe
NGINX_HEALTH_URL=http://127.0.0.1/ NGINX_HEALTH_URL=http://127.0.0.1/
# Optional LAN client whitelist for the main nginx entry.
# Empty means allow all. Use semicolon/comma/space-separated IPs or CIDRs, for example:
# NGINX_ALLOWED_CLIENT_IPS=192.168.1.10;192.168.1.23;192.168.1.0/24
NGINX_ALLOWED_CLIENT_IPS=
LICENSE_PATH= LICENSE_PATH=
CORS_ORIGINS=http://127.0.0.1:5173,http://localhost CORS_ORIGINS=http://127.0.0.1:5173,http://localhost
@@ -63,13 +67,15 @@ MONITOR_DINSAR_DIRS=D:\DInSARResult
ORBIT_SOURCE_DIRS=D:\LT1_data_lsarorbit;D:\Sentinel1_EOF_Pool ORBIT_SOURCE_DIRS=D:\LT1_data_lsarorbit;D:\Sentinel1_EOF_Pool
MONITOR_ORBIT_DIR=D:\LT1_data_lsarorbit MONITOR_ORBIT_DIR=D:\LT1_data_lsarorbit
GF3_ARCHIVE_SOURCE_DIRS=D:\GF3_L1A_Image_Zip GF3_ARCHIVE_SOURCE_DIRS=D:\production_inputs\gf3\archives
GF3_ARCHIVE_EXTS=.zip,.tar,.tar.gz,.tgz GF3_ARCHIVE_EXTS=.zip,.tar,.tar.gz,.tgz
GF3_UNPACK_DELETE_ARCHIVE=true GF3_UNPACK_DELETE_ARCHIVE=true
GF3_SOURCE_DIRS=D:\GF3_L1A_Image GF3_LEGACY_GDAL_ENABLED=false
GF3_SARSCAPE_NATIVE_DIRS=D:\GF3_L2_ENVI_Binary_Pool GF3_SOURCE_DIRS=
GF3_STORAGE_DIRS=D:\GF3_L2_Image_Pool GF3_SARSCAPE_NATIVE_DIRS=D:\production_results\gf3\sarscape_native
GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\.codex_tmp\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe GF3_STORAGE_DIRS=D:\production_results\gf3\standard_l2
GF3_SARSCAPE_RUNTIME_DIR=D:\production_runtime\gf3\sarscape_runtime
GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\third_party\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe
GF3_SARSCAPE_IDLRT_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe GF3_SARSCAPE_IDLRT_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe
GF3_SARSCAPE_DEM_PATH=D:\DEM\COPDEM_GLO30_China_4326_DEM GF3_SARSCAPE_DEM_PATH=D:\DEM\COPDEM_GLO30_China_4326_DEM
GF3_SARSCAPE_POLARIZATIONS=HH,HV GF3_SARSCAPE_POLARIZATIONS=HH,HV
@@ -114,8 +120,7 @@ SRTM_DEM_DIR=D:\SRTM30m
GF3_GEO_DEM_PATH=D:\DEM\gf3_dem.jp2 GF3_GEO_DEM_PATH=D:\DEM\gf3_dem.jp2
WATER_RESULTS_DIR=D:\WaterResult WATER_RESULTS_DIR=D:\WaterResult
SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready
SAR_ANALYSIS_WORK_ROOT=D:\production_results\sar_analysis_work SAR_ANALYSIS_WORK_ROOT=D:\production_runtime\sar_analysis_work
SAR_ANALYSIS_PREVIEW_ROOT=D:\production_results\sar_analysis_preview
SAR_ANALYSIS_NODATA_VALUE=-9999 SAR_ANALYSIS_NODATA_VALUE=-9999
SAR_ANALYSIS_OUTPUT_COG=true SAR_ANALYSIS_OUTPUT_COG=true
@@ -125,7 +130,7 @@ SAR_ANALYSIS_OUTPUT_COG=true
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
IDL_EXECUTABLE=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idl.exe IDL_EXECUTABLE=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idl.exe
IDL_WORKBENCH_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlde.exe IDL_WORKBENCH_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlde.exe
IDL_WORKER_RUNTIME_DIR=D:\Code\Insar_management_system_v2\backend\runtime\idl_worker IDL_WORKER_RUNTIME_DIR=D:\production_runtime\idl_worker
IDL_WORKER_DEFAULT_TIMEOUT_SECONDS=14400 IDL_WORKER_DEFAULT_TIMEOUT_SECONDS=14400
IDL_WORKER_MAX_TIMEOUT_SECONDS=43200 IDL_WORKER_MAX_TIMEOUT_SECONDS=43200
@@ -153,7 +158,7 @@ ENVI_STABILITY_MAX_WAIT=3600
WSL_DISTRO=Ubuntu-24.04 WSL_DISTRO=Ubuntu-24.04
WSL_SHARED_CONDA_ENV=insar_wsl_v1 WSL_SHARED_CONDA_ENV=insar_wsl_v1
WSL_SHARED_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python WSL_SHARED_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python
WSL_BROKER_JOB_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\wsl_jobs WSL_BROKER_JOB_ROOT=D:\production_runtime\wsl_jobs
ISCE2_RUNTIME_ID=isce2_runtime_v1 ISCE2_RUNTIME_ID=isce2_runtime_v1
PYINT_RUNTIME_ID=gamma_pyint_runtime_v1 PYINT_RUNTIME_ID=gamma_pyint_runtime_v1
@@ -170,7 +175,7 @@ ISCE2_STRIPMAP_APP=/home/administrator/miniconda3/envs/insar_wsl_v1/lib/python3.
ISCE2_PIPELINE_SCRIPT= ISCE2_PIPELINE_SCRIPT=
# ISCE2 should point to the prepared WGS84 DEM after the one-time conversion. # ISCE2 should point to the prepared WGS84 DEM after the one-time conversion.
ISCE2_DEM_PATH=D:\SRTM30m\SRTMDEM_RSP_SARscape.wgs84 ISCE2_DEM_PATH=D:\SRTM30m\SRTMDEM_RSP_SARscape.wgs84
ISCE2_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\isce2_work ISCE2_WORK_ROOT=D:\production_runtime\isce2_work
ISCE2_OUTPUT_ROOT=D:\production_results\dinsar ISCE2_OUTPUT_ROOT=D:\production_results\dinsar
ISCE2_PER_TASK_TIMEOUT_SECONDS=43200 ISCE2_PER_TASK_TIMEOUT_SECONDS=43200
ISCE2_SMOKE_TEST_ENABLED=false ISCE2_SMOKE_TEST_ENABLED=false
@@ -218,10 +223,10 @@ PYINT_WSL_DISTRO=Ubuntu-24.04
PYINT_WSL_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python PYINT_WSL_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python
PYINT_HOME=D:\Code\Insar_management_system_v2\third_party\PyINT PYINT_HOME=D:\Code\Insar_management_system_v2\third_party\PyINT
PYINT_APP_SCRIPT=D:\Code\Insar_management_system_v2\third_party\PyINT\pyint\pyintApp.py PYINT_APP_SCRIPT=D:\Code\Insar_management_system_v2\third_party\PyINT\pyint\pyintApp.py
PYINT_TEMPLATE_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\pyint_templates PYINT_TEMPLATE_ROOT=D:\production_runtime\pyint_templates
PYINT_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\pyint_work PYINT_WORK_ROOT=D:\production_runtime\pyint_work
PYINT_OUTPUT_ROOT=D:\production_results\dinsar PYINT_OUTPUT_ROOT=D:\production_results\dinsar
PYINT_DEM_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\pyint_dem PYINT_DEM_ROOT=D:\production_runtime\pyint_dem
PYINT_DEM_MODE=local_fabdem PYINT_DEM_MODE=local_fabdem
PYINT_FABDEM_ROOT= PYINT_FABDEM_ROOT=
# When PYINT_DEM_MODE=prepared_file, point this to the same prepared WGS84 DEM. # When PYINT_DEM_MODE=prepared_file, point this to the same prepared WGS84 DEM.
@@ -267,8 +272,9 @@ GAMMA_SBAS_RUNTIME_ID=gamma_sbas_runtime_v1
GAMMA_SBAS_WSL_DISTRO=Ubuntu-24.04 GAMMA_SBAS_WSL_DISTRO=Ubuntu-24.04
GAMMA_SBAS_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python GAMMA_SBAS_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python
GAMMA_SBAS_ENV_SCRIPT=D:\Code\Insar_management_system_v2\deploy\wsl\profiles\gamma_env.sh GAMMA_SBAS_ENV_SCRIPT=D:\Code\Insar_management_system_v2\deploy\wsl\profiles\gamma_env.sh
GAMMA_SBAS_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\sbas_insar_production GAMMA_SBAS_WORK_ROOT=D:\production_runtime\sbas_insar_work
GAMMA_SBAS_PRODUCT_ROOT=D:\production_results\timeseries\sbas GAMMA_SBAS_PRODUCT_ROOT=D:\production_results\timeseries\sbas
GAMMA_SBAS_TRIAL_ROOT=D:\production_runtime\gamma_ipta_trials
GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT=D:\Code\Insar_management_system_v2\backend\templates\gamma_sbas GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT=D:\Code\Insar_management_system_v2\backend\templates\gamma_sbas
GAMMA_SBAS_SOURCE_ROOTS=D:\LuTan1_Image_Pool GAMMA_SBAS_SOURCE_ROOTS=D:\LuTan1_Image_Pool
GAMMA_SBAS_ORBIT_ROOTS=D:\orbit_pools\envi GAMMA_SBAS_ORBIT_ROOTS=D:\orbit_pools\envi
@@ -290,7 +296,7 @@ TIMESERIES_ENABLED=false
TIMESERIES_WSL_DISTRO=Ubuntu-24.04 TIMESERIES_WSL_DISTRO=Ubuntu-24.04
TIMESERIES_ENV_NAME=insar_wsl_v1 TIMESERIES_ENV_NAME=insar_wsl_v1
TIMESERIES_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python TIMESERIES_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python
TIMESERIES_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\timeseries_work TIMESERIES_WORK_ROOT=D:\production_runtime\timeseries_work
TIMESERIES_DEM_PATH=D:\SRTM30m\SRTMDEM_RSP_SARscape.wgs84 TIMESERIES_DEM_PATH=D:\SRTM30m\SRTMDEM_RSP_SARscape.wgs84
TIMESERIES_ORBIT_POOL_ISCE2=D:\orbit_pools\isce2 TIMESERIES_ORBIT_POOL_ISCE2=D:\orbit_pools\isce2
TIMESERIES_EXPERIMENT_ROOT= TIMESERIES_EXPERIMENT_ROOT=
@@ -320,6 +326,24 @@ JOB_WORKER_STALE_RUNNING_SECONDS=7200
JOB_WORKER_HEARTBEAT_INTERVAL=5 JOB_WORKER_HEARTBEAT_INTERVAL=5
# -----------------------------------------------------------------------------
# Runtime cleanup policy
# -----------------------------------------------------------------------------
# scripts/cleanup_runtime.py is dry-run by default. Pass --delete --yes to remove
# unprotected generated artifacts. Paths referenced by product/catalog tables are
# protected automatically.
RUNTIME_CLEANUP_PYINT_WORK_RETENTION_DAYS=14
RUNTIME_CLEANUP_PYINT_DEM_RETENTION_DAYS=60
RUNTIME_CLEANUP_SBAS_RUN_RETENTION_DAYS=30
RUNTIME_CLEANUP_TRIAL_RETENTION_DAYS=7
RUNTIME_CLEANUP_IDL_LOG_RETENTION_DAYS=30
RUNTIME_CLEANUP_WSL_JOB_RETENTION_DAYS=14
RUNTIME_CLEANUP_TIMESERIES_WORK_RETENTION_DAYS=30
RUNTIME_CLEANUP_IMAGE_CACHE_RETENTION_DAYS=60
RUNTIME_CLEANUP_LOG_RETENTION_DAYS=30
RUNTIME_CLEANUP_FRONTEND_DIST_RETENTION_DAYS=0
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# 解包 # 解包
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -358,5 +382,11 @@ DEFAULT_VLM_MODEL=qwen3-vl:30b
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# 前端 tile server # 前端 tile server
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
VITE_TILE_SERVER_URL=http://127.0.0.1:8910 # Empty means same-origin /tiles, routed by the main nginx proxy.
VITE_TILE_SERVER_URL=
VITE_TILE_SERVER_TOKEN=change_me VITE_TILE_SERVER_TOKEN=change_me
TILE_SERVER_AUTO_START=true
TILE_SERVER_AUTO_STOP=true
TILE_SERVER_ROOT=D:\Code\tile-server
TILE_SERVER_START_SCRIPT=start-all.bat
TILE_SERVER_STOP_SCRIPT=stop-all.bat
+1
View File
@@ -65,6 +65,7 @@ tmp_dir_coreg_*/
# Local third-party runtime bundles # Local third-party runtime bundles
third_party/LandSAR/ third_party/LandSAR/
third_party/GF3_L1A_To_L2_pipeline/
# Large local datasets / installers # Large local datasets / installers
Data/ Data/
+90 -22
View File
@@ -42,7 +42,7 @@ def _infer_conda_env_name_from_python(path: str | None) -> str:
def _default_idl_runtime_dir() -> str: def _default_idl_runtime_dir() -> str:
return os.path.join(_BACKEND_DIR, "runtime", "idl_worker") return os.path.join(_default_runtime_root(_PROJECT_ROOT), "idl_worker")
def _resolve_idl_runtime_dir(value: str | None) -> str: def _resolve_idl_runtime_dir(value: str | None) -> str:
@@ -77,6 +77,26 @@ def _default_result_publish_root(project_root: str) -> str:
return os.path.join(normalized_root, "production_results") return os.path.join(normalized_root, "production_results")
def _default_input_root(project_root: str) -> str:
normalized_root = os.path.normpath(project_root)
drive, _tail = os.path.splitdrive(normalized_root)
if drive:
return os.path.join(drive + os.sep, "production_inputs")
return os.path.join(normalized_root, "production_inputs")
def _default_runtime_root(project_root: str) -> str:
normalized_root = os.path.normpath(project_root)
drive, _tail = os.path.splitdrive(normalized_root)
if drive:
return os.path.join(drive + os.sep, "production_runtime")
return os.path.join(normalized_root, "runtime")
def _default_runtime_dir(project_root: str, *parts: str) -> str:
return os.path.join(_default_runtime_root(project_root), *parts)
def _read_env_pairs(env_path: str) -> dict[str, str]: def _read_env_pairs(env_path: str) -> dict[str, str]:
env_map: dict[str, str] = {} env_map: dict[str, str] = {}
if not os.path.isfile(env_path): if not os.path.isfile(env_path):
@@ -197,7 +217,6 @@ class Settings(BaseSettings):
WATER_RESULTS_DIR: str = "" WATER_RESULTS_DIR: str = ""
SAR_ANALYSIS_READY_ROOT: str = "" SAR_ANALYSIS_READY_ROOT: str = ""
SAR_ANALYSIS_WORK_ROOT: str = "" SAR_ANALYSIS_WORK_ROOT: str = ""
SAR_ANALYSIS_PREVIEW_ROOT: str = ""
SAR_ANALYSIS_NODATA_VALUE: float = -9999.0 SAR_ANALYSIS_NODATA_VALUE: float = -9999.0
SAR_ANALYSIS_OUTPUT_COG: bool = True SAR_ANALYSIS_OUTPUT_COG: bool = True
@@ -206,9 +225,11 @@ class Settings(BaseSettings):
GF3_ARCHIVE_SOURCE_DIRS: str = "" GF3_ARCHIVE_SOURCE_DIRS: str = ""
GF3_ARCHIVE_EXTS: str = ".zip,.tar,.tar.gz,.tgz" GF3_ARCHIVE_EXTS: str = ".zip,.tar,.tar.gz,.tgz"
GF3_UNPACK_DELETE_ARCHIVE: bool = True GF3_UNPACK_DELETE_ARCHIVE: bool = True
GF3_LEGACY_GDAL_ENABLED: bool = False
GF3_SOURCE_DIRS: str = "" GF3_SOURCE_DIRS: str = ""
GF3_SARSCAPE_NATIVE_DIRS: str = "" GF3_SARSCAPE_NATIVE_DIRS: str = ""
GF3_STORAGE_DIRS: str = "" GF3_STORAGE_DIRS: str = ""
GF3_SARSCAPE_RUNTIME_DIR: str = ""
GF3_SARSCAPE_WRAPPER_EXE: str = "" GF3_SARSCAPE_WRAPPER_EXE: str = ""
GF3_SARSCAPE_IDLRT_PATH: str = r"C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe" GF3_SARSCAPE_IDLRT_PATH: str = r"C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe"
GF3_SARSCAPE_DEM_PATH: str = "" GF3_SARSCAPE_DEM_PATH: str = ""
@@ -340,6 +361,7 @@ class Settings(BaseSettings):
GAMMA_SBAS_ENV_SCRIPT: str = "" GAMMA_SBAS_ENV_SCRIPT: str = ""
GAMMA_SBAS_WORK_ROOT: str = "" GAMMA_SBAS_WORK_ROOT: str = ""
GAMMA_SBAS_PRODUCT_ROOT: str = "" GAMMA_SBAS_PRODUCT_ROOT: str = ""
GAMMA_SBAS_TRIAL_ROOT: str = ""
GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT: str = "" GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT: str = ""
GAMMA_SBAS_SOURCE_ROOTS: str = "" GAMMA_SBAS_SOURCE_ROOTS: str = ""
GAMMA_SBAS_ORBIT_ROOTS: str = "" GAMMA_SBAS_ORBIT_ROOTS: str = ""
@@ -420,19 +442,13 @@ class Settings(BaseSettings):
object.__setattr__( object.__setattr__(
self, self,
"SAR_ANALYSIS_READY_ROOT", "SAR_ANALYSIS_READY_ROOT",
os.path.join(backend_dir, "runtime", "sar_analysis_ready"), os.path.join(_default_result_publish_root(project_root), "sar_analysis_ready"),
) )
if not self.SAR_ANALYSIS_WORK_ROOT: if not self.SAR_ANALYSIS_WORK_ROOT:
object.__setattr__( object.__setattr__(
self, self,
"SAR_ANALYSIS_WORK_ROOT", "SAR_ANALYSIS_WORK_ROOT",
os.path.join(backend_dir, "runtime", "sar_analysis_work"), _default_runtime_dir(project_root, "sar_analysis_work"),
)
if not self.SAR_ANALYSIS_PREVIEW_ROOT:
object.__setattr__(
self,
"SAR_ANALYSIS_PREVIEW_ROOT",
os.path.join(backend_dir, "runtime", "sar_analysis_preview"),
) )
object.__setattr__( object.__setattr__(
self, self,
@@ -441,11 +457,29 @@ class Settings(BaseSettings):
) )
if not self.SRTM_DEM_DIR: if not self.SRTM_DEM_DIR:
object.__setattr__(self, "SRTM_DEM_DIR", os.path.join(backend_dir, "dem_data")) object.__setattr__(self, "SRTM_DEM_DIR", os.path.join(backend_dir, "dem_data"))
if not self.GF3_ARCHIVE_SOURCE_DIRS:
object.__setattr__(
self,
"GF3_ARCHIVE_SOURCE_DIRS",
os.path.join(_default_input_root(project_root), "gf3", "archives"),
)
if not self.GF3_SARSCAPE_NATIVE_DIRS:
object.__setattr__(
self,
"GF3_SARSCAPE_NATIVE_DIRS",
os.path.join(_default_result_publish_root(project_root), "gf3", "sarscape_native"),
)
if not self.GF3_STORAGE_DIRS: if not self.GF3_STORAGE_DIRS:
object.__setattr__( object.__setattr__(
self, self,
"GF3_STORAGE_DIRS", "GF3_STORAGE_DIRS",
os.path.join(backend_dir, "gf3_results"), os.path.join(_default_result_publish_root(project_root), "gf3", "standard_l2"),
)
if not self.GF3_SARSCAPE_RUNTIME_DIR:
object.__setattr__(
self,
"GF3_SARSCAPE_RUNTIME_DIR",
_default_runtime_dir(project_root, "gf3", "sarscape_runtime"),
) )
if not self.ORBIT_QUARANTINE_DIR and self.MONITOR_ORBIT_DIR: if not self.ORBIT_QUARANTINE_DIR and self.MONITOR_ORBIT_DIR:
object.__setattr__( object.__setattr__(
@@ -496,7 +530,7 @@ class Settings(BaseSettings):
object.__setattr__( object.__setattr__(
self, self,
"ISCE2_WORK_ROOT", "ISCE2_WORK_ROOT",
os.path.join(backend_dir, "runtime", "isce2_work"), _default_runtime_dir(project_root, "isce2_work"),
) )
if not self.ISCE2_OUTPUT_ROOT: if not self.ISCE2_OUTPUT_ROOT:
object.__setattr__( object.__setattr__(
@@ -582,7 +616,7 @@ class Settings(BaseSettings):
object.__setattr__( object.__setattr__(
self, self,
"WSL_BROKER_JOB_ROOT", "WSL_BROKER_JOB_ROOT",
os.path.join(backend_dir, "runtime", "wsl_jobs"), _default_runtime_dir(project_root, "wsl_jobs"),
) )
if not self.ISCE2_RUNTIME_ID: if not self.ISCE2_RUNTIME_ID:
object.__setattr__(self, "ISCE2_RUNTIME_ID", "isce2_runtime_v1") object.__setattr__(self, "ISCE2_RUNTIME_ID", "isce2_runtime_v1")
@@ -608,13 +642,13 @@ class Settings(BaseSettings):
object.__setattr__( object.__setattr__(
self, self,
"PYINT_TEMPLATE_ROOT", "PYINT_TEMPLATE_ROOT",
os.path.join(backend_dir, "runtime", "pyint_templates"), _default_runtime_dir(project_root, "pyint_templates"),
) )
if not self.PYINT_WORK_ROOT: if not self.PYINT_WORK_ROOT:
object.__setattr__( object.__setattr__(
self, self,
"PYINT_WORK_ROOT", "PYINT_WORK_ROOT",
os.path.join(backend_dir, "runtime", "pyint_work"), _default_runtime_dir(project_root, "pyint_work"),
) )
if not self.PYINT_OUTPUT_ROOT: if not self.PYINT_OUTPUT_ROOT:
object.__setattr__( object.__setattr__(
@@ -626,7 +660,7 @@ class Settings(BaseSettings):
object.__setattr__( object.__setattr__(
self, self,
"PYINT_DEM_ROOT", "PYINT_DEM_ROOT",
os.path.join(backend_dir, "runtime", "pyint_dem"), os.path.join(_default_runtime_root(project_root), "pyint_dem"),
) )
pyint_dem_mode = str(self.PYINT_DEM_MODE or "local_fabdem").strip().lower() or "local_fabdem" pyint_dem_mode = str(self.PYINT_DEM_MODE or "local_fabdem").strip().lower() or "local_fabdem"
if pyint_dem_mode not in {"local_fabdem", "opentopo", "prepared_file"}: if pyint_dem_mode not in {"local_fabdem", "opentopo", "prepared_file"}:
@@ -743,7 +777,7 @@ class Settings(BaseSettings):
object.__setattr__( object.__setattr__(
self, self,
"GAMMA_SBAS_WORK_ROOT", "GAMMA_SBAS_WORK_ROOT",
os.path.join(backend_dir, "runtime", "sbas_insar_production"), os.path.join(_default_runtime_root(project_root), "sbas_insar_work"),
) )
if not self.GAMMA_SBAS_PRODUCT_ROOT: if not self.GAMMA_SBAS_PRODUCT_ROOT:
object.__setattr__( object.__setattr__(
@@ -751,6 +785,12 @@ class Settings(BaseSettings):
"GAMMA_SBAS_PRODUCT_ROOT", "GAMMA_SBAS_PRODUCT_ROOT",
os.path.join(self.TIMESERIES_PRODUCT_DIR, "sbas"), os.path.join(self.TIMESERIES_PRODUCT_DIR, "sbas"),
) )
if not self.GAMMA_SBAS_TRIAL_ROOT:
object.__setattr__(
self,
"GAMMA_SBAS_TRIAL_ROOT",
os.path.join(_default_runtime_root(project_root), "gamma_ipta_trials"),
)
if not self.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT: if not self.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT:
object.__setattr__( object.__setattr__(
self, self,
@@ -848,7 +888,7 @@ class Settings(BaseSettings):
object.__setattr__( object.__setattr__(
self, self,
"TIMESERIES_WORK_ROOT", "TIMESERIES_WORK_ROOT",
os.path.join(backend_dir, "runtime", "timeseries_work"), _default_runtime_dir(project_root, "timeseries_work"),
) )
if not self.TIMESERIES_DEM_PATH: if not self.TIMESERIES_DEM_PATH:
object.__setattr__(self, "TIMESERIES_DEM_PATH", self.ISCE2_DEM_PATH) object.__setattr__(self, "TIMESERIES_DEM_PATH", self.ISCE2_DEM_PATH)
@@ -947,7 +987,14 @@ class Settings(BaseSettings):
os.makedirs(settings.RESULT_QUARANTINE_ROOT, exist_ok=True) os.makedirs(settings.RESULT_QUARANTINE_ROOT, exist_ok=True)
os.makedirs(settings.SAR_ANALYSIS_READY_ROOT, exist_ok=True) os.makedirs(settings.SAR_ANALYSIS_READY_ROOT, exist_ok=True)
os.makedirs(settings.SAR_ANALYSIS_WORK_ROOT, exist_ok=True) os.makedirs(settings.SAR_ANALYSIS_WORK_ROOT, exist_ok=True)
os.makedirs(settings.SAR_ANALYSIS_PREVIEW_ROOT, exist_ok=True) for path in split_env_paths(settings.GF3_ARCHIVE_SOURCE_DIRS):
os.makedirs(path, exist_ok=True)
for path in split_env_paths(settings.GF3_SARSCAPE_NATIVE_DIRS):
os.makedirs(path, exist_ok=True)
for path in split_env_paths(settings.GF3_STORAGE_DIRS):
os.makedirs(path, exist_ok=True)
if settings.GF3_SARSCAPE_RUNTIME_DIR:
os.makedirs(settings.GF3_SARSCAPE_RUNTIME_DIR, exist_ok=True)
os.makedirs(settings.WSL_BROKER_JOB_ROOT, exist_ok=True) os.makedirs(settings.WSL_BROKER_JOB_ROOT, exist_ok=True)
os.makedirs(settings.PYINT_TEMPLATE_ROOT, exist_ok=True) os.makedirs(settings.PYINT_TEMPLATE_ROOT, exist_ok=True)
os.makedirs(settings.PYINT_WORK_ROOT, exist_ok=True) os.makedirs(settings.PYINT_WORK_ROOT, exist_ok=True)
@@ -958,6 +1005,7 @@ class Settings(BaseSettings):
if settings.GAMMA_SBAS_ENABLED: if settings.GAMMA_SBAS_ENABLED:
os.makedirs(settings.GAMMA_SBAS_WORK_ROOT, exist_ok=True) os.makedirs(settings.GAMMA_SBAS_WORK_ROOT, exist_ok=True)
os.makedirs(settings.GAMMA_SBAS_PRODUCT_ROOT, exist_ok=True) os.makedirs(settings.GAMMA_SBAS_PRODUCT_ROOT, exist_ok=True)
os.makedirs(settings.GAMMA_SBAS_TRIAL_ROOT, exist_ok=True)
os.makedirs(settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT, exist_ok=True) os.makedirs(settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT, exist_ok=True)
if settings.LANDSAR_SBAS_ENABLED: if settings.LANDSAR_SBAS_ENABLED:
os.makedirs(settings.LANDSAR_SBAS_WORK_ROOT, exist_ok=True) os.makedirs(settings.LANDSAR_SBAS_WORK_ROOT, exist_ok=True)
@@ -1115,7 +1163,6 @@ def validate_runtime_config() -> dict[str, Any]:
_check_path(label="WATER_RESULTS_DIR", value=settings.WATER_RESULTS_DIR, errors=errors, warnings=warnings, expect_file=False) _check_path(label="WATER_RESULTS_DIR", value=settings.WATER_RESULTS_DIR, errors=errors, warnings=warnings, expect_file=False)
_check_path(label="SAR_ANALYSIS_READY_ROOT", value=settings.SAR_ANALYSIS_READY_ROOT, errors=errors, warnings=warnings, expect_file=False) _check_path(label="SAR_ANALYSIS_READY_ROOT", value=settings.SAR_ANALYSIS_READY_ROOT, errors=errors, warnings=warnings, expect_file=False)
_check_path(label="SAR_ANALYSIS_WORK_ROOT", value=settings.SAR_ANALYSIS_WORK_ROOT, errors=errors, warnings=warnings, expect_file=False) _check_path(label="SAR_ANALYSIS_WORK_ROOT", value=settings.SAR_ANALYSIS_WORK_ROOT, errors=errors, warnings=warnings, expect_file=False)
_check_path(label="SAR_ANALYSIS_PREVIEW_ROOT", value=settings.SAR_ANALYSIS_PREVIEW_ROOT, errors=errors, warnings=warnings, expect_file=False)
_check_path(label="MONITOR_ORBIT_DIR", value=settings.MONITOR_ORBIT_DIR, errors=errors, warnings=warnings, expect_file=False) _check_path(label="MONITOR_ORBIT_DIR", value=settings.MONITOR_ORBIT_DIR, errors=errors, warnings=warnings, expect_file=False)
for label, value in ( for label, value in (
("SOURCE_PRODUCT_DIRS", settings.SOURCE_PRODUCT_DIRS), ("SOURCE_PRODUCT_DIRS", settings.SOURCE_PRODUCT_DIRS),
@@ -1143,10 +1190,9 @@ def validate_runtime_config() -> dict[str, Any]:
("INSAR_STORAGE_DIRS", settings.INSAR_STORAGE_DIRS), ("INSAR_STORAGE_DIRS", settings.INSAR_STORAGE_DIRS),
("MONITOR_RADAR_DIRS", settings.MONITOR_RADAR_DIRS), ("MONITOR_RADAR_DIRS", settings.MONITOR_RADAR_DIRS),
("MONITOR_DINSAR_DIRS", settings.MONITOR_DINSAR_DIRS), ("MONITOR_DINSAR_DIRS", settings.MONITOR_DINSAR_DIRS),
("GF3_ARCHIVE_SOURCE_DIRS", settings.GF3_ARCHIVE_SOURCE_DIRS),
("GF3_SOURCE_DIRS", settings.GF3_SOURCE_DIRS),
("GF3_SARSCAPE_NATIVE_DIRS", settings.GF3_SARSCAPE_NATIVE_DIRS), ("GF3_SARSCAPE_NATIVE_DIRS", settings.GF3_SARSCAPE_NATIVE_DIRS),
("GF3_STORAGE_DIRS", settings.GF3_STORAGE_DIRS), ("GF3_STORAGE_DIRS", settings.GF3_STORAGE_DIRS),
("GF3_SARSCAPE_RUNTIME_DIR", settings.GF3_SARSCAPE_RUNTIME_DIR),
): ):
values = split_env_paths(raw_value) values = split_env_paths(raw_value)
if not values: if not values:
@@ -1155,6 +1201,21 @@ def validate_runtime_config() -> dict[str, Any]:
for item in values: for item in values:
_check_path(label=label, value=item, errors=errors, warnings=warnings, expect_file=False) _check_path(label=label, value=item, errors=errors, warnings=warnings, expect_file=False)
gf3_archive_dirs = split_env_paths(settings.GF3_ARCHIVE_SOURCE_DIRS)
if not gf3_archive_dirs:
warnings.append("GF3_ARCHIVE_SOURCE_DIRS 未配置;无法触发 GF3 SARscape 生产,只能扫描已有原生结果。")
for item in gf3_archive_dirs:
_check_path(label="GF3_ARCHIVE_SOURCE_DIRS", value=item, errors=errors, warnings=warnings, expect_file=False)
if settings.GF3_LEGACY_GDAL_ENABLED:
legacy_dirs = split_env_paths(settings.GF3_SOURCE_DIRS)
if not legacy_dirs:
errors.append("GF3_LEGACY_GDAL_ENABLED=true but GF3_SOURCE_DIRS is not configured.")
for item in legacy_dirs:
_check_path(label="GF3_SOURCE_DIRS", value=item, errors=errors, warnings=warnings, expect_file=False)
elif split_env_paths(settings.GF3_SOURCE_DIRS):
warnings.append("GF3_SOURCE_DIRS is configured but GF3_LEGACY_GDAL_ENABLED=false; legacy GDAL preprocessing is disabled.")
if settings.ISCE2_ENABLED: if settings.ISCE2_ENABLED:
if not settings.ISCE2_WSL_DISTRO: if not settings.ISCE2_WSL_DISTRO:
errors.append("ISCE2_ENABLED=true 但 ISCE2_WSL_DISTRO 未配置。") errors.append("ISCE2_ENABLED=true 但 ISCE2_WSL_DISTRO 未配置。")
@@ -1279,6 +1340,13 @@ def validate_runtime_config() -> dict[str, Any]:
warnings=warnings, warnings=warnings,
expect_file=False, expect_file=False,
) )
_check_path(
label="GAMMA_SBAS_TRIAL_ROOT",
value=settings.GAMMA_SBAS_TRIAL_ROOT,
errors=errors,
warnings=warnings,
expect_file=False,
)
_check_path( _check_path(
label="GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT", label="GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT",
value=settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT, value=settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT,
+1 -1
View File
@@ -88,7 +88,7 @@ async def lifespan(app: FastAPI):
sbas_catalog_bootstrap = await sbas_insar_catalog_service.bootstrap_catalog_on_startup_clean() sbas_catalog_bootstrap = await sbas_insar_catalog_service.bootstrap_catalog_on_startup_clean()
except Exception as exc: except Exception as exc:
sbas_catalog_bootstrap = { sbas_catalog_bootstrap = {
"storage_root": settings.GAMMA_SBAS_WORK_ROOT, "storage_root": settings.GAMMA_SBAS_PRODUCT_ROOT,
"manifest_count": 0, "manifest_count": 0,
"db_count": 0, "db_count": 0,
"needs_rebuild": False, "needs_rebuild": False,
+5 -1
View File
@@ -260,4 +260,8 @@ async def get_dinsar_product_preview(
if not preview_path or not os.path.isfile(preview_path): if not preview_path or not os.path.isfile(preview_path):
raise HTTPException(status_code=404, detail="Preview not found") raise HTTPException(status_code=404, detail="Preview not found")
media_type = mimetypes.guess_type(preview_path)[0] or "application/octet-stream" media_type = mimetypes.guess_type(preview_path)[0] or "application/octet-stream"
return FileResponse(preview_path, media_type=media_type) return FileResponse(
preview_path,
media_type=media_type,
headers={"Cache-Control": "public, max-age=31536000, immutable"},
)
+18
View File
@@ -43,8 +43,10 @@ class MonitorConfig(BaseModel):
dinsar_dirs: List[str] = [] dinsar_dirs: List[str] = []
gf3_archive_source_dirs: List[str] = [] gf3_archive_source_dirs: List[str] = []
gf3_source_dirs: List[str] = [] gf3_source_dirs: List[str] = []
gf3_legacy_gdal_enabled: bool = False
gf3_sarscape_native_dirs: List[str] = [] gf3_sarscape_native_dirs: List[str] = []
gf3_storage_dirs: List[str] = [] gf3_storage_dirs: List[str] = []
gf3_sarscape_runtime_dir: Optional[str] = None
gf3_sarscape_wrapper_exe: Optional[str] = None gf3_sarscape_wrapper_exe: Optional[str] = None
gf3_sarscape_idlrt_path: Optional[str] = None gf3_sarscape_idlrt_path: Optional[str] = None
gf3_sarscape_dem_path: Optional[str] = None gf3_sarscape_dem_path: Optional[str] = None
@@ -159,6 +161,14 @@ async def run_gf3_batch_process(admin_user: AuthUserORM = Depends(_require_admin
""" """
批量 GF3 L1A→L2 处理:扫描 GF3_SOURCE_DIRS,过滤已处理的,逐个处理并自动入库。 批量 GF3 L1A→L2 处理:扫描 GF3_SOURCE_DIRS,过滤已处理的,逐个处理并自动入库。
""" """
if not settings.GF3_LEGACY_GDAL_ENABLED:
raise HTTPException(
status_code=409,
detail=(
"Legacy GF3 Python/GDAL preprocessing is disabled. "
"Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly."
),
)
gf3_source_dirs = MONITOR_CONFIG.get("gf3_source_dirs") or [] gf3_source_dirs = MONITOR_CONFIG.get("gf3_source_dirs") or []
if not gf3_source_dirs: if not gf3_source_dirs:
raise HTTPException(status_code=400, detail="GF3_SOURCE_DIRS is not configured.") raise HTTPException(status_code=400, detail="GF3_SOURCE_DIRS is not configured.")
@@ -331,6 +341,14 @@ async def run_gf3_unpack(
""" """
将 GF3 压缩包池解包到 GF3_SOURCE_DIRS,作为后续 L1A→L2 预处理输入。 将 GF3 压缩包池解包到 GF3_SOURCE_DIRS,作为后续 L1A→L2 预处理输入。
""" """
if not settings.GF3_LEGACY_GDAL_ENABLED:
raise HTTPException(
status_code=409,
detail=(
"Legacy GF3 archive unpack is disabled. "
"Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly."
),
)
gf3_archive_source_dirs = MONITOR_CONFIG.get("gf3_archive_source_dirs") or [] gf3_archive_source_dirs = MONITOR_CONFIG.get("gf3_archive_source_dirs") or []
gf3_source_dirs = MONITOR_CONFIG.get("gf3_source_dirs") or [] gf3_source_dirs = MONITOR_CONFIG.get("gf3_source_dirs") or []
if not gf3_archive_source_dirs: if not gf3_archive_source_dirs:
+5 -1
View File
@@ -142,4 +142,8 @@ async def get_psinsar_product_preview(
preview_path = str(detail.get("preview_path") or "").strip() preview_path = str(detail.get("preview_path") or "").strip()
if not preview_path or not os.path.isfile(preview_path): if not preview_path or not os.path.isfile(preview_path):
raise HTTPException(status_code=404, detail="Preview not found") raise HTTPException(status_code=404, detail="Preview not found")
return FileResponse(preview_path, media_type="image/png") return FileResponse(
preview_path,
media_type="image/png",
headers={"Cache-Control": "public, max-age=31536000, immutable"},
)
@@ -479,6 +479,7 @@ async def get_landsar_sbas_run_artifact(run_id: str, relative_path: str):
artifact_path, artifact_path,
media_type=media_type, media_type=media_type,
filename=artifact_path.name, filename=artifact_path.name,
headers={"Cache-Control": "public, max-age=31536000, immutable"},
) )
@@ -1022,6 +1023,7 @@ async def get_sbas_insar_run_artifact(run_id: str, relative_path: str):
artifact_path, artifact_path,
media_type=media_type, media_type=media_type,
filename=artifact_path.name, filename=artifact_path.name,
headers={"Cache-Control": "public, max-age=31536000, immutable"},
) )
@@ -1054,4 +1056,5 @@ async def get_sbas_insar_artifact(trial_id: str, relative_path: str):
artifact_path, artifact_path,
media_type=media_type, media_type=media_type,
filename=artifact_path.name, filename=artifact_path.name,
headers={"Cache-Control": "public, max-age=31536000, immutable"},
) )
+3 -1
View File
@@ -21,6 +21,7 @@ from .dependencies import _add_operation_audit_log, _get_current_user, _require_
router = APIRouter() router = APIRouter()
STATIC_ASSET_CACHE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
class SbasInsarCatalogRebuildRequest(BaseModel): class SbasInsarCatalogRebuildRequest(BaseModel):
@@ -189,7 +190,7 @@ async def get_sbas_insar_product_preview(
preview_path = str(detail.get("preview_path") or "").strip() preview_path = str(detail.get("preview_path") or "").strip()
if not preview_path or not os.path.isfile(preview_path): if not preview_path or not os.path.isfile(preview_path):
raise HTTPException(status_code=404, detail="Preview not found") raise HTTPException(status_code=404, detail="Preview not found")
return FileResponse(preview_path, media_type="image/png") return FileResponse(preview_path, media_type="image/png", headers=STATIC_ASSET_CACHE_HEADERS)
@router.get("/sbas-insar-products/{product_db_id}/assets/{asset_id}") @router.get("/sbas-insar-products/{product_db_id}/assets/{asset_id}")
@@ -209,4 +210,5 @@ async def get_sbas_insar_product_asset(
asset.absolute_path, asset.absolute_path,
media_type=asset.media_type or "application/octet-stream", media_type=asset.media_type or "application/octet-stream",
filename=asset.asset_name or os.path.basename(asset.absolute_path), filename=asset.asset_name or os.path.basename(asset.absolute_path),
headers=STATIC_ASSET_CACHE_HEADERS,
) )
+8
View File
@@ -902,6 +902,14 @@ async def submit_gf3_process(
admin_user: AuthUserORM = Depends(_require_admin), admin_user: AuthUserORM = Depends(_require_admin),
): ):
"""提交 GF3 L1A→L2 处理任务(辐射定标 + RPC 几何校正)。""" """提交 GF3 L1A→L2 处理任务(辐射定标 + RPC 几何校正)。"""
if not settings.GF3_LEGACY_GDAL_ENABLED:
raise HTTPException(
status_code=409,
detail=(
"Legacy GF3 Python/GDAL preprocessing is disabled. "
"Use /monitor/gf3-sarscape-produce or /monitor/gf3-sarscape-sync."
),
)
if not os.path.exists(req.input_dir): if not os.path.exists(req.input_dir):
raise HTTPException(status_code=400, detail=f"输入路径不存在: {req.input_dir}") raise HTTPException(status_code=400, detail=f"输入路径不存在: {req.input_dir}")
+2
View File
@@ -21,8 +21,10 @@ MONITOR_CONFIG = {
# GF3 链路 # GF3 链路
"gf3_archive_source_dirs": split_env_paths(settings.GF3_ARCHIVE_SOURCE_DIRS), "gf3_archive_source_dirs": split_env_paths(settings.GF3_ARCHIVE_SOURCE_DIRS),
"gf3_source_dirs": split_env_paths(settings.GF3_SOURCE_DIRS), "gf3_source_dirs": split_env_paths(settings.GF3_SOURCE_DIRS),
"gf3_legacy_gdal_enabled": bool(settings.GF3_LEGACY_GDAL_ENABLED),
"gf3_sarscape_native_dirs": split_env_paths(settings.GF3_SARSCAPE_NATIVE_DIRS), "gf3_sarscape_native_dirs": split_env_paths(settings.GF3_SARSCAPE_NATIVE_DIRS),
"gf3_storage_dirs": split_env_paths(settings.GF3_STORAGE_DIRS), "gf3_storage_dirs": split_env_paths(settings.GF3_STORAGE_DIRS),
"gf3_sarscape_runtime_dir": settings.GF3_SARSCAPE_RUNTIME_DIR,
"gf3_sarscape_wrapper_exe": settings.GF3_SARSCAPE_WRAPPER_EXE, "gf3_sarscape_wrapper_exe": settings.GF3_SARSCAPE_WRAPPER_EXE,
"gf3_sarscape_idlrt_path": settings.GF3_SARSCAPE_IDLRT_PATH, "gf3_sarscape_idlrt_path": settings.GF3_SARSCAPE_IDLRT_PATH,
"gf3_sarscape_dem_path": settings.GF3_SARSCAPE_DEM_PATH or settings.GF3_GEO_DEM_PATH, "gf3_sarscape_dem_path": settings.GF3_SARSCAPE_DEM_PATH or settings.GF3_GEO_DEM_PATH,
@@ -115,7 +115,7 @@ def _resolve_config_path(raw: Any) -> Path | None:
def _fallback_wrapper_exe() -> Path | None: def _fallback_wrapper_exe() -> Path | None:
candidate = ( candidate = (
Path(settings.PROJECT_ROOT) Path(settings.PROJECT_ROOT)
/ ".codex_tmp" / "third_party"
/ "GF3_L1A_To_L2_pipeline" / "GF3_L1A_To_L2_pipeline"
/ "dist" / "dist"
/ "windows" / "windows"
@@ -443,7 +443,12 @@ def run_gf3_sarscape_production(
"results": [], "results": [],
} }
runtime_dir = native_root_path / ".gf3_runtime" configured_runtime_dir = _clean_text(getattr(settings, "GF3_SARSCAPE_RUNTIME_DIR", ""))
runtime_dir = (
Path(os.path.normpath(configured_runtime_dir)).resolve()
if configured_runtime_dir
else native_root_path / ".gf3_runtime"
)
runtime_dir.mkdir(parents=True, exist_ok=True) runtime_dir.mkdir(parents=True, exist_ok=True)
config_path = runtime_dir / "gf3wrapper.json" config_path = runtime_dir / "gf3wrapper.json"
env = os.environ.copy() env = os.environ.copy()
+11 -2
View File
@@ -969,7 +969,11 @@ async def _check_source_roots() -> Dict[str, Any]:
for path in split_env_paths(settings.GF3_SOURCE_DIRS): for path in split_env_paths(settings.GF3_SOURCE_DIRS):
status = _probe_directory_status(path) status = _probe_directory_status(path)
status["role"] = "gf3_l1a_source" status["role"] = (
"gf3_l1a_source"
if settings.GF3_LEGACY_GDAL_ENABLED
else "gf3_legacy_l1a_source_disabled"
)
items.append(status) items.append(status)
for path in split_env_paths(settings.GF3_SARSCAPE_NATIVE_DIRS): for path in split_env_paths(settings.GF3_SARSCAPE_NATIVE_DIRS):
@@ -982,6 +986,12 @@ async def _check_source_roots() -> Dict[str, Any]:
status["role"] = "gf3_l2_storage" status["role"] = "gf3_l2_storage"
items.append(status) items.append(status)
runtime_dir = str(getattr(settings, "GF3_SARSCAPE_RUNTIME_DIR", "") or "").strip()
if runtime_dir:
status = _probe_directory_status(runtime_dir)
status["role"] = "gf3_sarscape_runtime"
items.append(status)
wrapper_exe = str(settings.GF3_SARSCAPE_WRAPPER_EXE or "").strip() wrapper_exe = str(settings.GF3_SARSCAPE_WRAPPER_EXE or "").strip()
if wrapper_exe: if wrapper_exe:
status = _probe_file_status(wrapper_exe) status = _probe_file_status(wrapper_exe)
@@ -1017,7 +1027,6 @@ async def _check_sar_analysis_ready() -> Dict[str, Any]:
roots = { roots = {
"ready": _probe_directory_status(settings.SAR_ANALYSIS_READY_ROOT), "ready": _probe_directory_status(settings.SAR_ANALYSIS_READY_ROOT),
"work": _probe_directory_status(settings.SAR_ANALYSIS_WORK_ROOT), "work": _probe_directory_status(settings.SAR_ANALYSIS_WORK_ROOT),
"preview": _probe_directory_status(settings.SAR_ANALYSIS_PREVIEW_ROOT),
} }
for role, payload in roots.items(): for role, payload in roots.items():
payload["role"] = role payload["role"] = role
+18
View File
@@ -3612,6 +3612,12 @@ async def _handle_gf3_process(job: SystemJobORM) -> None:
"""GF3 L1A→L2 处理 job handler(辐射定标 + RPC 几何校正)。""" """GF3 L1A→L2 处理 job handler(辐射定标 + RPC 几何校正)。"""
from .gf3_service import run_gf3_l1a_to_l2 from .gf3_service import run_gf3_l1a_to_l2
if not settings.GF3_LEGACY_GDAL_ENABLED:
raise ValueError(
"Legacy GF3 Python/GDAL preprocessing is disabled. "
"Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly."
)
payload = job.payload or {} payload = job.payload or {}
processing_id = payload.get("processing_id") processing_id = payload.get("processing_id")
if not processing_id: if not processing_id:
@@ -3685,6 +3691,12 @@ async def _handle_gf3_unpack(job: SystemJobORM) -> None:
"""GF3 archive inbox -> persistent L1A source pool.""" """GF3 archive inbox -> persistent L1A source pool."""
from .gf3_unpack_service import run_gf3_archive_unpack from .gf3_unpack_service import run_gf3_archive_unpack
if not settings.GF3_LEGACY_GDAL_ENABLED:
raise ValueError(
"Legacy GF3 archive unpack is disabled. "
"Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly."
)
if not job.task_id: if not job.task_id:
raise ValueError("GF3_UNPACK requires task_id for progress tracking.") raise ValueError("GF3_UNPACK requires task_id for progress tracking.")
@@ -3746,6 +3758,12 @@ async def _handle_gf3_batch_process(job: SystemJobORM) -> None:
from .gf3_service import run_gf3_l1a_to_l2, register_l2_to_radar_data from .gf3_service import run_gf3_l1a_to_l2, register_l2_to_radar_data
from .sar_analysis_ready_service import standardize_gf3_l2_for_radar from .sar_analysis_ready_service import standardize_gf3_l2_for_radar
if not settings.GF3_LEGACY_GDAL_ENABLED:
raise ValueError(
"Legacy GF3 Python/GDAL preprocessing is disabled. "
"Use GF3 SARscape production or set GF3_LEGACY_GDAL_ENABLED=true explicitly."
)
payload = job.payload or {} payload = job.payload or {}
source_dirs = payload.get("source_dirs") or [] source_dirs = payload.get("source_dirs") or []
if not source_dirs: if not source_dirs:
@@ -271,6 +271,16 @@ def _build_root_specs_from_settings() -> List[RootSpec]:
scan_mode="scene_directory", scan_mode="scene_directory",
) )
) )
specs.extend(
_iter_single_root_specs(
env_var="GF3_SARSCAPE_RUNTIME_DIR",
path=settings.GF3_SARSCAPE_RUNTIME_DIR,
root_role="work_root_gf3_sarscape",
display_name="GF3 SARscape Runtime",
scan_mode="workspace",
owner_engine="sarscape",
)
)
specs.extend( specs.extend(
_iter_single_root_specs( _iter_single_root_specs(
env_var="SAR_ANALYSIS_READY_ROOT", env_var="SAR_ANALYSIS_READY_ROOT",
@@ -438,6 +448,46 @@ def _build_root_specs_from_settings() -> List[RootSpec]:
owner_engine="pyint", owner_engine="pyint",
) )
) )
specs.extend(
_iter_single_root_specs(
env_var="PYINT_DEM_ROOT",
path=settings.PYINT_DEM_ROOT,
root_role="dem_cache_root_pyint",
display_name="PyINT DEM Cache Root",
scan_mode="workspace",
owner_engine="pyint",
)
)
specs.extend(
_iter_single_root_specs(
env_var="GAMMA_SBAS_WORK_ROOT",
path=settings.GAMMA_SBAS_WORK_ROOT,
root_role="work_root_gamma_sbas",
display_name="Gamma SBAS Work Root",
scan_mode="workspace",
owner_engine="gamma",
)
)
specs.extend(
_iter_single_root_specs(
env_var="GAMMA_SBAS_PRODUCT_ROOT",
path=settings.GAMMA_SBAS_PRODUCT_ROOT,
root_role="publish_root_gamma_sbas",
display_name="Gamma SBAS Publish Root",
scan_mode="manifest_tree",
owner_engine="gamma",
)
)
specs.extend(
_iter_single_root_specs(
env_var="GAMMA_SBAS_TRIAL_ROOT",
path=settings.GAMMA_SBAS_TRIAL_ROOT,
root_role="trial_root_gamma_sbas",
display_name="Gamma SBAS Trial Root",
scan_mode="workspace",
owner_engine="gamma",
)
)
return specs return specs
@@ -289,6 +289,50 @@ def _read_gamma_int_param(path: Path, key: str) -> Optional[int]:
return _safe_int(_read_gamma_key_values(path).get(key)) return _safe_int(_read_gamma_key_values(path).get(key))
def _expert_gamma_work_run_dir(run_dir: Path) -> Path:
run_id = run_dir.name
candidates = [
run_dir,
Path(settings.GAMMA_SBAS_WORK_ROOT or "") / "runs" / run_id if settings.GAMMA_SBAS_WORK_ROOT else run_dir,
]
seen: set[str] = set()
for candidate in candidates:
normalized = _normalize_path(candidate)
if normalized in seen:
continue
seen.add(normalized)
if (
(candidate / "sbas" / "disp.TS_tab").is_file()
and (candidate / "sbas" / "mli.ave.par").is_file()
and any((candidate / "dem").glob("*.lt_fine"))
):
return candidate
return run_dir
def _expert_gamma_lookup_path(work_run_dir: Path, manifest: dict[str, Any]) -> Path:
lt_path = work_run_dir / "dem" / f"{manifest.get('reference_date') or ''}.lt_fine"
if lt_path.is_file():
return lt_path
candidates = sorted((work_run_dir / "dem").glob("*.lt_fine"))
return candidates[0] if candidates else lt_path
def _resolve_expert_gamma_tab_path(raw_path: str, *, work_run_dir: Path) -> Path:
path = Path(_wsl_path_to_windows(raw_path))
if path.is_file():
return path
run_id = work_run_dir.name
parts = list(path.parts)
if run_id in parts:
index = parts.index(run_id)
relative_parts = parts[index + 1 :]
candidate = work_run_dir.joinpath(*relative_parts)
if candidate.is_file():
return candidate
return path
def _haversine_m(lon1: float, lat1: float, lon2: float, lat2: float) -> float: def _haversine_m(lon1: float, lat1: float, lon2: float, lat2: float) -> float:
radius_m = 6371008.8 radius_m = 6371008.8
phi1 = math.radians(lat1) phi1 = math.radians(lat1)
@@ -762,12 +806,13 @@ def _read_geo_rate_window(
def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: float) -> dict[str, Any]: def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: float) -> dict[str, Any]:
source_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate.tif" source_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate.tif"
coverage_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate_rgb.tif" coverage_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate_rgb.tif"
lt_path = run_dir / "dem" / f"{_safe_read_json(run_dir / 'run_manifest.json').get('reference_date') or ''}.lt_fine" work_run_dir = _expert_gamma_work_run_dir(run_dir)
if not lt_path.is_file(): work_manifest = _safe_read_json(work_run_dir / "run_manifest.json")
candidates = sorted((run_dir / "dem").glob("*.lt_fine")) if not work_manifest:
lt_path = candidates[0] if candidates else lt_path work_manifest = _safe_read_json(run_dir / "run_manifest.json")
mli_par = run_dir / "sbas" / "mli.ave.par" lt_path = _expert_gamma_lookup_path(work_run_dir, work_manifest)
disp_tab = run_dir / "sbas" / "disp.TS_tab" mli_par = work_run_dir / "sbas" / "mli.ave.par"
disp_tab = work_run_dir / "sbas" / "disp.TS_tab"
if not source_path.is_file(): if not source_path.is_file():
raise FileNotFoundError("geo_los_def_rate.tif is missing") raise FileNotFoundError("geo_los_def_rate.tif is missing")
if not lt_path.is_file(): if not lt_path.is_file():
@@ -852,11 +897,13 @@ def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: floa
img_y = min(max(0, img_y), radar_lines - 1) img_y = min(max(0, img_y), radar_lines - 1)
raw_disp_paths = [ raw_disp_paths = [
Path(_wsl_path_to_windows(line.strip())) _resolve_expert_gamma_tab_path(line.strip(), work_run_dir=work_run_dir)
for line in disp_tab.read_text(encoding="utf-8", errors="ignore").splitlines() for line in disp_tab.read_text(encoding="utf-8", errors="ignore").splitlines()
if line.strip() if line.strip()
] ]
dates = _read_expert_sbas_dates(run_dir) dates = _read_expert_sbas_dates(work_run_dir)
if not dates:
dates = _read_expert_sbas_dates(run_dir)
displacements: list[dict[str, Any]] = [] displacements: list[dict[str, Any]] = []
for index, disp_path in enumerate(raw_disp_paths): for index, disp_path in enumerate(raw_disp_paths):
value_m = _read_radar_float32(disp_path, width=radar_width, img_x=img_x, img_y=img_y) value_m = _read_radar_float32(disp_path, width=radar_width, img_x=img_x, img_y=img_y)
@@ -872,6 +919,8 @@ def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: floa
return { return {
"schema": "insar.gamma-sbas-point-query/v1", "schema": "insar.gamma-sbas-point-query/v1",
"source_tool": "disp.TS_tab_radar_pixel_sample", "source_tool": "disp.TS_tab_radar_pixel_sample",
"source_run_dir": str(run_dir),
"work_run_dir": str(work_run_dir),
"query": {"lon": lon, "lat": lat}, "query": {"lon": lon, "lat": lat},
"matched": { "matched": {
**valid_choice, **valid_choice,
@@ -906,11 +955,11 @@ def _locate_radar_points_in_geocoded_product(run_dir: Path, points: list[dict[st
source_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate.tif" source_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate.tif"
coverage_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate_rgb.tif" coverage_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate_rgb.tif"
manifest = _safe_read_json(run_dir / "run_manifest.json") work_run_dir = _expert_gamma_work_run_dir(run_dir)
lt_path = run_dir / "dem" / f"{manifest.get('reference_date') or ''}.lt_fine" manifest = _safe_read_json(work_run_dir / "run_manifest.json")
if not lt_path.is_file(): if not manifest:
candidates = sorted((run_dir / "dem").glob("*.lt_fine")) manifest = _safe_read_json(run_dir / "run_manifest.json")
lt_path = candidates[0] if candidates else lt_path lt_path = _expert_gamma_lookup_path(work_run_dir, manifest)
if not source_path.is_file() or not lt_path.is_file(): if not source_path.is_file() or not lt_path.is_file():
return {} return {}
@@ -1982,6 +2031,12 @@ def _stack_dates_from_manifest(stack_manifest: dict[str, Any], manifest: dict[st
class SbasInsarCatalogService: class SbasInsarCatalogService:
def get_run_root(self) -> str: def get_run_root(self) -> str:
root = Path(settings.GAMMA_SBAS_PRODUCT_ROOT or Path(settings.TIMESERIES_PRODUCT_DIR) / "sbas")
run_root = root / "runs"
run_root.mkdir(parents=True, exist_ok=True)
return _normalize_path(run_root)
def get_work_run_root(self) -> str:
root = Path(settings.GAMMA_SBAS_WORK_ROOT or Path(settings.BACKEND_DIR) / "runtime" / "sbas_insar_production") root = Path(settings.GAMMA_SBAS_WORK_ROOT or Path(settings.BACKEND_DIR) / "runtime" / "sbas_insar_production")
run_root = root / "runs" run_root = root / "runs"
run_root.mkdir(parents=True, exist_ok=True) run_root.mkdir(parents=True, exist_ok=True)
@@ -1989,6 +2044,9 @@ class SbasInsarCatalogService:
def get_run_roots(self) -> list[str]: def get_run_roots(self) -> list[str]:
roots = [self.get_run_root()] roots = [self.get_run_root()]
work_root = self.get_work_run_root()
if work_root not in roots:
roots.append(work_root)
try: try:
landsar_root = landsar_sbas_service.configured_run_root() landsar_root = landsar_sbas_service.configured_run_root()
if landsar_root not in roots: if landsar_root not in roots:
@@ -1999,17 +2057,17 @@ class SbasInsarCatalogService:
def _iter_run_manifest_paths(self, run_root: Optional[str] = None) -> list[str]: def _iter_run_manifest_paths(self, run_root: Optional[str] = None) -> list[str]:
roots = [run_root] if run_root else self.get_run_roots() roots = [run_root] if run_root else self.get_run_roots()
manifest_paths: list[str] = [] manifest_paths_by_run: dict[str, str] = {}
for raw_root in roots: for raw_root in roots:
root = Path(raw_root) root = Path(raw_root)
if not root.is_dir(): if not root.is_dir():
continue continue
manifest_paths.extend( for path in sorted(root.glob("*/run_manifest.json")):
_normalize_path(path) if not self._is_publish_ready(path.parent, _safe_read_json(path)):
for path in sorted(root.glob("*/run_manifest.json")) continue
if self._is_publish_ready(path.parent, _safe_read_json(path)) run_id = path.parent.name
) manifest_paths_by_run.setdefault(run_id, _normalize_path(path))
return sorted(dict.fromkeys(manifest_paths)) return list(manifest_paths_by_run.values())
@staticmethod @staticmethod
def _is_landsar_manifest(manifest: dict[str, Any]) -> bool: def _is_landsar_manifest(manifest: dict[str, Any]) -> bool:
@@ -2295,9 +2353,12 @@ class SbasInsarCatalogService:
if self._is_landsar_manifest(manifest): if self._is_landsar_manifest(manifest):
return self._build_landsar_product(run_dir, manifest_file, manifest) return self._build_landsar_product(run_dir, manifest_file, manifest)
detail = sbas_insar_production_service.get_run_detail(run_dir.name)
coverage = detail.get("geographic_coverage") or {}
stack_manifest = _safe_read_json(run_dir / "stack_manifest.json") stack_manifest = _safe_read_json(run_dir / "stack_manifest.json")
try:
detail = sbas_insar_production_service.get_run_detail(run_dir.name)
coverage = detail.get("geographic_coverage") or {}
except FileNotFoundError:
coverage = sbas_insar_production_service._build_run_geographic_coverage(run_dir, manifest)
monitor_summary = _safe_read_json(run_dir / "monitor_points_summary.json") monitor_summary = _safe_read_json(run_dir / "monitor_points_summary.json")
workflow_summary = _safe_read_json(run_dir / "workflow_summary.json") workflow_summary = _safe_read_json(run_dir / "workflow_summary.json")
is_expert_gamma = self._is_expert_gamma_manifest(manifest, run_dir) is_expert_gamma = self._is_expert_gamma_manifest(manifest, run_dir)
@@ -786,8 +786,20 @@ class SbasInsarProductionService:
} }
def __init__(self) -> None: def __init__(self) -> None:
self.trial_root = Path(settings.BACKEND_DIR) / "runtime" / "gamma_ipta_trials" project_drive = Path(settings.PROJECT_ROOT).drive
self.production_root = Path(settings.GAMMA_SBAS_WORK_ROOT or (Path(settings.BACKEND_DIR) / "runtime" / "sbas_insar_production")) default_runtime_root = Path(f"{project_drive}\\production_runtime") if project_drive else Path(settings.PROJECT_ROOT) / "runtime"
self.trial_root = Path(settings.GAMMA_SBAS_TRIAL_ROOT or default_runtime_root / "gamma_ipta_trials")
self.production_root = Path(settings.GAMMA_SBAS_WORK_ROOT or default_runtime_root / "sbas_insar_work")
self.product_root = Path(settings.GAMMA_SBAS_PRODUCT_ROOT or Path(settings.TIMESERIES_PRODUCT_DIR) / "sbas")
def product_run_root(self) -> Path:
return self.product_root / "runs"
def product_run_dir(self, run_id: str) -> Path:
clean_id = str(run_id or "").strip()
if not clean_id or Path(clean_id).name != clean_id:
raise ValueError("invalid run id")
return self.product_run_root() / clean_id
def get_capabilities(self) -> dict[str, Any]: def get_capabilities(self) -> dict[str, Any]:
return { return {
@@ -797,6 +809,7 @@ class SbasInsarProductionService:
"implementation_state": "expert_manifest_script_runner_primary", "implementation_state": "expert_manifest_script_runner_primary",
"trial_root": str(self.trial_root), "trial_root": str(self.trial_root),
"production_root": str(self.production_root), "production_root": str(self.production_root),
"product_root": str(self.product_root),
"min_common_overlap_ratio": self._effective_min_common_overlap_ratio(None), "min_common_overlap_ratio": self._effective_min_common_overlap_ratio(None),
"workflow_runner": { "workflow_runner": {
"enabled": bool(settings.GAMMA_SBAS_ENABLED), "enabled": bool(settings.GAMMA_SBAS_ENABLED),
@@ -3412,6 +3425,8 @@ class SbasInsarProductionService:
self._write_json(run_dir / "monitor_points_summary.json", summary) self._write_json(run_dir / "monitor_points_summary.json", summary)
self._write_json(manifest_path, manifest) self._write_json(manifest_path, manifest)
self._refresh_command_manifest_after_monitor_points(run_dir, manifest) self._refresh_command_manifest_after_monitor_points(run_dir, manifest)
if manifest["status"] == "MONITOR_POINTS_READY":
self.sync_product_package(run_id)
return self.get_run_detail(run_id) return self.get_run_detail(run_id)
def list_trial_runs(self) -> dict[str, Any]: def list_trial_runs(self) -> dict[str, Any]:
@@ -3828,6 +3843,8 @@ class SbasInsarProductionService:
run_manifest["next_stage"] = "fix_workflow" run_manifest["next_stage"] = "fix_workflow"
self._write_json(manifest_path, run_manifest) self._write_json(manifest_path, run_manifest)
self._write_json(run_dir / "workflow_summary.json", summary) self._write_json(run_dir / "workflow_summary.json", summary)
if run_manifest.get("status") == "WORKFLOW_COMPLETED":
self.sync_product_package(run_id)
return self.get_run_detail(run_id) return self.get_run_detail(run_id)
@staticmethod @staticmethod
@@ -5204,6 +5221,7 @@ class SbasInsarProductionService:
errors.append(f"{label} does not point to an existing Gamma DEM + .par pair: {raw_path}") errors.append(f"{label} does not point to an existing Gamma DEM + .par pair: {raw_path}")
cache_roots = [ cache_roots = [
Path(settings.PYINT_DEM_ROOT),
Path(settings.BACKEND_DIR) / "runtime" / "pyint_dem", Path(settings.BACKEND_DIR) / "runtime" / "pyint_dem",
Path(settings.BACKEND_DIR) / "runtime" / "pyint_dem_cache", Path(settings.BACKEND_DIR) / "runtime" / "pyint_dem_cache",
] ]
@@ -7138,6 +7156,118 @@ class SbasInsarProductionService:
out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
return out_path return out_path
@staticmethod
def _copy_file_if_newer(source: Path, target: Path) -> bool:
if not source.is_file():
return False
target.parent.mkdir(parents=True, exist_ok=True)
try:
source_stat = source.stat()
target_stat = target.stat() if target.exists() else None
if (
target_stat is not None
and target_stat.st_size == source_stat.st_size
and int(target_stat.st_mtime) >= int(source_stat.st_mtime)
):
return False
shutil.copy2(source, target)
return True
except OSError:
shutil.copy2(source, target)
return True
@staticmethod
def _copy_tree_files(source_dir: Path, target_dir: Path) -> tuple[int, int]:
if not source_dir.is_dir():
return 0, 0
copied = 0
skipped = 0
for source in source_dir.rglob("*"):
if not source.is_file():
continue
target = target_dir / source.relative_to(source_dir)
if SbasInsarProductionService._copy_file_if_newer(source, target):
copied += 1
else:
skipped += 1
return copied, skipped
def sync_product_package(self, run_id: str) -> dict[str, Any]:
run_dir = self._resolve_run_dir(run_id)
product_dir = self.product_run_dir(run_dir.name)
product_dir.mkdir(parents=True, exist_ok=True)
copied = 0
skipped = 0
for relative in (
"run_manifest.json",
"stack_manifest.json",
"pair_network.json",
"workflow_summary.json",
"monitor_points_summary.json",
"product_summary.json",
"quality_summary.json",
"gamma_command_manifest.json",
"expert_command_audit.json",
):
if self._copy_file_if_newer(run_dir / relative, product_dir / relative):
copied += 1
else:
skipped += 1
for dirname in ("publish",):
tree_copied, tree_skipped = self._copy_tree_files(run_dir / dirname, product_dir / dirname)
copied += tree_copied
skipped += tree_skipped
for relative in (
"diff_dir/bprep_file.png",
"diff_dir/mean.cc_mask.bmp",
"sbas/final_unw_tab",
):
if self._copy_file_if_newer(run_dir / relative, product_dir / relative):
copied += 1
else:
skipped += 1
final_tab = run_dir / "sbas" / "final_unw_tab"
diff_dir = run_dir / "diff_dir"
pair_ids: list[str] = []
if final_tab.is_file():
for line in final_tab.read_text(encoding="utf-8", errors="ignore").splitlines():
raw_path = line.strip().split()[0] if line.strip() else ""
if not raw_path:
continue
name = Path(self._path_to_windows(raw_path) or raw_path).name
pair_id = name.replace(".unw.atmsub_1", "").replace(".unw", "")
if pair_id and pair_id not in pair_ids:
pair_ids.append(pair_id)
qcs = [diff_dir / f"{pair_id}.adf.unw.bmp" for pair_id in pair_ids if (diff_dir / f"{pair_id}.adf.unw.bmp").is_file()]
if not qcs and diff_dir.is_dir():
qcs = sorted(diff_dir.glob("*.adf.unw.bmp"))
if len(qcs) > 3:
last_index = len(qcs) - 1
indexes = sorted({round(index * last_index / 2) for index in range(3)})
qcs = [qcs[index] for index in indexes]
for source in qcs:
target = product_dir / source.relative_to(run_dir)
if self._copy_file_if_newer(source, target):
copied += 1
else:
skipped += 1
marker = {
"schema": "insar.gamma-sbas-product-package/v1",
"run_id": run_dir.name,
"source_run_dir": str(run_dir),
"product_run_dir": str(product_dir),
"copied_files": copied,
"skipped_files": skipped,
"synced_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
}
self._write_json(product_dir / "product_package_manifest.json", marker)
return marker
@staticmethod @staticmethod
def _write_json(path: Path, payload: dict[str, Any]) -> Path: def _write_json(path: Path, payload: dict[str, Any]) -> Path:
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
+9 -1
View File
@@ -24,6 +24,14 @@ def _project_file_windows(*relative_parts: str) -> str:
return os.path.normpath(str(Path(settings.PROJECT_ROOT, *relative_parts))) return os.path.normpath(str(Path(settings.PROJECT_ROOT, *relative_parts)))
def _default_runtime_root_windows() -> str:
root = os.path.normpath(str(settings.PROJECT_ROOT))
drive, _tail = os.path.splitdrive(root)
if drive:
return os.path.join(drive + os.sep, "production_runtime")
return os.path.join(root, "runtime")
@dataclass(frozen=True) @dataclass(frozen=True)
class WslRuntimeDefinition: class WslRuntimeDefinition:
runtime_id: str runtime_id: str
@@ -106,7 +114,7 @@ def build_wsl_runtime_registry() -> WslRuntimeRegistry:
).strip() ).strip()
broker_job_root_windows = os.path.normpath( broker_job_root_windows = os.path.normpath(
settings.WSL_BROKER_JOB_ROOT settings.WSL_BROKER_JOB_ROOT
or os.path.join(settings.BACKEND_DIR, "runtime", "wsl_jobs") or os.path.join(_default_runtime_root_windows(), "wsl_jobs")
) )
broker_job_root_wsl = _windows_path_to_wsl_mount(broker_job_root_windows) broker_job_root_wsl = _windows_path_to_wsl_mount(broker_job_root_windows)
@@ -0,0 +1,152 @@
# Basemap Tile Server Proxy and LAN Access
Date: 2026-06-13
This document records the local tile-server integration used by the InSAR Management System v2 Windows launcher.
## Goals
- Start and stop `D:\Code\tile-server` from the main `start_system.bat` / `stop_system.bat` workflow.
- Serve basemap tiles through the main Nginx entry instead of exposing tile-server directly to browsers.
- Keep tile access token configuration in local `.env`; do not commit real tokens.
- Allow optional LAN client IP whitelisting.
- Show a clear restricted-access page for non-whitelisted clients.
## Runtime Topology
```text
LAN browser
-> http://<server-ip>/
-> main nginx :80
-> frontend dist
-> /api/ -> FastAPI backend on 127.0.0.1:<PORT>
-> /tiles/ -> tile-server on 127.0.0.1:8910
-> /geojson/ -> tile-server on 127.0.0.1:8910
```
The tile-server can stay bound to `127.0.0.1:8910`. LAN clients should not call `8910` directly.
## `.env` Settings
Tile-server frontend URL should normally be empty, which means same-origin routing through Nginx:
```env
VITE_TILE_SERVER_URL=
VITE_TILE_SERVER_TOKEN=change_me
TILE_SERVER_AUTO_START=true
TILE_SERVER_AUTO_STOP=true
TILE_SERVER_ROOT=D:\Code\tile-server
TILE_SERVER_START_SCRIPT=start-all.bat
TILE_SERVER_STOP_SCRIPT=stop-all.bat
```
The real `VITE_TILE_SERVER_TOKEN` belongs only in local `.env`.
## LAN IP Whitelist
Use `NGINX_ALLOWED_CLIENT_IPS` to restrict clients:
```env
NGINX_ALLOWED_CLIENT_IPS=192.168.1.10;192.168.1.23
```
Accepted separators are semicolon, comma, or whitespace. CIDR values are also accepted:
```env
NGINX_ALLOWED_CLIENT_IPS=192.168.1.0/28
```
Empty value means no client IP restriction:
```env
NGINX_ALLOWED_CLIENT_IPS=
```
When the whitelist is enabled, the launcher generates `nginx/client_allow.conf` with `allow` rules plus `deny all`.
## Downstream Router Cases
When client devices are behind another router, write the IP that the InSAR server actually sees.
### NAT router mode
If a downstream router has an upstream LAN address such as `192.168.1.63`, and its clients are hidden behind NAT, Nginx will see every downstream client as:
```text
192.168.1.63
```
Whitelist the router IP:
```env
NGINX_ALLOWED_CLIENT_IPS=192.168.1.63
```
This allows all clients behind that NAT router because the server cannot distinguish them by their private downstream IPs.
### AP or bridge mode
If the downstream device works as an AP/bridge, clients usually receive addresses from the main LAN, for example:
```text
192.168.1.80
192.168.1.81
```
Whitelist the actual device IPs:
```env
NGINX_ALLOWED_CLIENT_IPS=192.168.1.80;192.168.1.81
```
### Routed subnet without NAT
If the downstream router forwards another subnet without NAT, the server may see the real downstream subnet addresses, for example:
```text
192.168.63.10
192.168.63.11
```
Whitelist exact client IPs:
```env
NGINX_ALLOWED_CLIENT_IPS=192.168.63.10;192.168.63.11
```
Or whitelist the whole downstream subnet when that is acceptable:
```env
NGINX_ALLOWED_CLIENT_IPS=192.168.63.0/24
```
### How to decide
Open the system from the downstream device before whitelisting it. The restricted-access page displays the detected client IP. Put that displayed IP, or its trusted CIDR range, into `NGINX_ALLOWED_CLIENT_IPS`.
## Restricted Client Page
Nginx maps `403` responses to `nginx/access_denied.html`. The page displays:
- access restricted message
- detected client IP
- requested host
- operator hint to update `NGINX_ALLOWED_CLIENT_IPS`
## Applying Changes
After changing `.env`, run `start_system.bat` or reload Nginx:
```powershell
C:\nginx-1.29.6\nginx.exe -t -c D:\Code\Insar_management_system_v2\nginx\nginx.conf
C:\nginx-1.29.6\nginx.exe -s reload -c D:\Code\Insar_management_system_v2\nginx\nginx.conf
```
From an allowed client:
```text
http://<server-ip>/
http://<server-ip>/tiles/gaode_image/7/107/42.webp?token=<token>
```
From a denied client, Nginx should return the restricted-access page with HTTP 403.
+31 -5
View File
@@ -111,11 +111,37 @@ ORBIT_POOL_LANDSAR=
IDL_EXECUTABLE=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idl.exe IDL_EXECUTABLE=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idl.exe
IDL_WORKBENCH_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlde.exe IDL_WORKBENCH_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlde.exe
IDL_DINSAR_DEM_BASE_FILE=D:\SRTM30m\SRTMDEM_RSP_SARscape IDL_DINSAR_DEM_BASE_FILE=D:\SRTM30m\SRTMDEM_RSP_SARscape
IDL_WORKER_RUNTIME_DIR=D:\Code\Insar_management_system_v2\backend\runtime\idl_worker IDL_WORKER_RUNTIME_DIR=D:\production_runtime\idl_worker
ENVI_TASK_TIMEOUT_SECONDS=21600 ENVI_TASK_TIMEOUT_SECONDS=21600
``` ```
### 3.5 WSL 共享运行时 ### 3.5 GF3 SARscape 生产
GF3 当前主线是“ENVI/SARscape 生产原生 `_geo` 证据层,平台标准化成 GeoTIFF 后入库和供洪涝/水体算法消费”。旧 Python/GDAL L1A 预处理链路默认关闭。
```env
GF3_ARCHIVE_SOURCE_DIRS=D:\production_inputs\gf3\archives
GF3_LEGACY_GDAL_ENABLED=false
GF3_SOURCE_DIRS=
GF3_SARSCAPE_NATIVE_DIRS=D:\production_results\gf3\sarscape_native
GF3_STORAGE_DIRS=D:\production_results\gf3\standard_l2
GF3_SARSCAPE_RUNTIME_DIR=D:\production_runtime\gf3\sarscape_runtime
GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\third_party\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe
GF3_SARSCAPE_IDLRT_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe
GF3_SARSCAPE_DEM_PATH=D:\DEM\COPDEM_GLO30_China_4326_DEM
GF3_SARSCAPE_POLARIZATIONS=HH,HV
GF3_SARSCAPE_AUTO_STANDARDIZE=true
GF3_SARSCAPE_CLEAN_AFTER_SUCCESS=true
```
说明:
- `GF3_SARSCAPE_NATIVE_DIRS` 长期保留 `_geo``.hdr``.sml`、快视、KML、日志和 manifest。
- `GF3_STORAGE_DIRS` 是标准 L2 GeoTIFF 池,后续入库、预览、洪涝/水体分析优先消费这里和 `SAR_ANALYSIS_READY_ROOT`
- `GF3_SARSCAPE_RUNTIME_DIR` 只放 wrapper 配置和运行时临时文件,不应混入原生结果池。
- 如确需恢复旧 L1A 解包/预处理,需要同时设置 `GF3_LEGACY_GDAL_ENABLED=true``GF3_SOURCE_DIRS`
### 3.6 WSL 共享运行时
当前推荐采用“一套共享 distro + 一套共享 conda 环境”模式。 当前推荐采用“一套共享 distro + 一套共享 conda 环境”模式。
@@ -123,7 +149,7 @@ ENVI_TASK_TIMEOUT_SECONDS=21600
WSL_DISTRO=Ubuntu-24.04 WSL_DISTRO=Ubuntu-24.04
WSL_SHARED_CONDA_ENV=insar_wsl_v1 WSL_SHARED_CONDA_ENV=insar_wsl_v1
WSL_SHARED_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python WSL_SHARED_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python
WSL_BROKER_JOB_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\wsl_jobs WSL_BROKER_JOB_ROOT=D:\production_runtime\wsl_jobs
ISCE2_RUNTIME_ID=isce2_runtime_v1 ISCE2_RUNTIME_ID=isce2_runtime_v1
PYINT_RUNTIME_ID=gamma_pyint_runtime_v1 PYINT_RUNTIME_ID=gamma_pyint_runtime_v1
@@ -138,7 +164,7 @@ PYINT_RUNTIME_ID=gamma_pyint_runtime_v1
使用共享 pythonrunner 为 `deploy/wsl/runners/gamma_pyint_runner.py` 使用共享 pythonrunner 为 `deploy/wsl/runners/gamma_pyint_runner.py`
Gamma 固定环境脚本为 `deploy/wsl/profiles/gamma_env.sh` Gamma 固定环境脚本为 `deploy/wsl/profiles/gamma_env.sh`
### 3.6 ISCE2 D-InSAR 与旧时序兼容 ### 3.7 ISCE2 D-InSAR 与旧时序兼容
```env ```env
ISCE2_ENABLED=false ISCE2_ENABLED=false
@@ -156,7 +182,7 @@ TIMESERIES_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python
- 旧 ISCE2/MintPy 时序生产链默认关闭,不再作为 SBAS 生产入口。 - 旧 ISCE2/MintPy 时序生产链默认关闭,不再作为 SBAS 生产入口。
- 如果必须做历史链路对比,需要显式开启 `TIMESERIES_ENABLED=true` 并提供完整旧脚本路径。 - 如果必须做历史链路对比,需要显式开启 `TIMESERIES_ENABLED=true` 并提供完整旧脚本路径。
### 3.7 Gamma / PyINT / SBAS ### 3.8 Gamma / PyINT / SBAS
```env ```env
PYINT_ENABLED=true PYINT_ENABLED=true
@@ -0,0 +1,212 @@
# D-InSAR 三引擎 Task_Pool 生产链路重构设计
更新日期:2026-06-14
## 1. 结论
D-InSAR 后续生产只保留三类核心引擎:
- `sarscape`ENVI + SARscape。
- `landsar`LandSAR D-InSAR。
- `pyint`Gamma / PyINT。
`isce2` 从 D-InSAR 正式生产链路退出。后端运行入口、引擎注册、任务类型、结果发布特殊分支、前端引擎选择和结果筛选都需要清理。已有 ISCE2 历史结果不在本次设计中直接删除,默认进入 legacy 只读语义:不参与新任务分发、不在默认结果管理视图中展示,只在显式历史查询或迁移脚本中处理。
三条保留链路统一走 Task_Pool 任务格式。LandSAR 和 ENVI + SARscape 只处理陆探 LT-1 数据;Gamma / PyINT 同时支持 LT-1 和 Sentinel-1。
## 2. 当前代码事实
当前系统已经具备多引擎基础,但还没有完成 Task_Pool 一致化:
- `backend/app/routers/dinsar_production.py` 暴露 D-InSAR 多引擎生产接口,当前仍接受 `sarscape / isce2 / pyint / landsar`
- `backend/app/dinsar_engines/registry.py` 仍注册 `Isce2Engine`
- `backend/app/services/dinsar_production_service.py` 已经按 engine/profile 生成运行项、当前指针和未完成判断,但 `_task_type_for_engine``_workflow_name_for_engine``_workflow_step_name_for_engine` 仍包含 ISCE2。
- `backend/app/services/job_handlers.py` 同时存在 SARscape、ISCE2、PyINT、LandSAR 的 job handler。
- `backend/app/dinsar_engines/landsar_engine.py` 已经围绕 LT-1 `Task_*/Input_Data``master/slave` 原始目录做输入检查。
- `backend/app/dinsar_engines/pyint_engine.py` 支持 `lt1_gamma_dinsar``s1_gamma_dinsar` profile,但未完成判断目前偏向 LT-1 profile,需要按 profile 收敛。
- `backend/app/services/result_catalog_service.py` 的结果目录、manifest、current 指针和 `result_products` 表已经能承载多引擎多次运行,但展示层仍是“一个引擎结果一条记录”。
- `backend/app/routers/task_batches.py` 的 D-InSAR batch/item 表保存了配对身份和任务状态,但没有保存或返回各引擎结果矩阵,也没有显式 Task_Pool 物理发布状态。
- `frontend/src/components/DinsarCatalogPanel.jsx``frontend/src/utils/dinsarEngines.js` 仍把 ISCE2 当作当前引擎之一,结果管理也是平铺展示。
## 3. 目标 Task_Pool 契约
D-InSAR 正式生产入口统一接受 Task_Pool 任务目录,不再把任意平铺目录作为正式生产输入。兼容导入可以保留在迁移工具或单独入口,不能作为标准链路。
推荐根路径:
```text
D:\Task_Pool\DInSAR\
Task_<master_date>_<slave_date>[_suffix]\
task_manifest.json
.dinsar_pair.json
master\
...
slave\
...
Input_Data\
...
```
目录语义:
- `task_manifest.json`:Task_Pool 任务清单,记录任务来源、配对身份、数据类型、允许引擎、各引擎结果快照和分发状态。新实现应写入;旧任务可由 `.dinsar_pair.json` 和目录结构推断。
- `.dinsar_pair.json`:现有配对侧车文件,继续作为 `pair_key``pair_uid``network_run_id``network_edge_id` 等身份信息的本地来源。
- `master/``slave/`:标准主从影像资产目录。LT-1 放陆探原始包或解包后的 xml/tifSentinel-1 放 SAFE/ZIP 及可解析的轨道资产引用。
- `Input_Data/`LandSAR 运行所需的 LT-1 输入目录。可以由 `master/slave` 导入生成,但生成关系必须写入 manifest,避免后续误判源数据。
任务身份以 `pair_key` 为稳定主键,`network_edge_id` 为配对网络中的边身份,`run_key` 为一次具体生产实例。已有代码中的 `.dinsar_run.json` 继续用于单次运行元数据。
## 4. 引擎能力矩阵
| 引擎 | engine_code | 数据类型 | 输入目录 | 结果 profile |
| --- | --- | --- | --- | --- |
| ENVI + SARscape | `sarscape` | LT-1 | `Task_*/master` + `Task_*/slave` | `custom6``metatask` |
| LandSAR | `landsar` | LT-1 | `Task_*/Input_Data`,或从 `master/slave` 导入生成 | `lt1_dinsar` |
| Gamma / PyINT | `pyint` | LT-1 | `Task_*/master` + `Task_*/slave` | `lt1_gamma_dinsar` |
| Gamma / PyINT | `pyint` | Sentinel-1 | `Task_*/master` + `Task_*/slave`,配套轨道资产 | `s1_gamma_dinsar` |
生产入口需要按数据类型和 profile 做硬约束:
- LT-1 任务允许 `sarscape``landsar``pyint`
- Sentinel-1 任务只允许 `pyint`
- `sarscape``landsar` 对 Sentinel-1 返回明确不可分发状态,而不是进入运行后失败。
- `pyint` 的未完成判断必须按 profile 区分,不能只检查 `lt1_gamma_dinsar`
## 5. 配对发布与完成状态
当前 `/task-batches/dinsar` 只保存配对条目,不知道每个 engine 是否已经产出结果。后续需要在“发布到 Task_Pool”和“查看任务池条目”两个位置补齐结果矩阵。
建议在 D-InSAR task item 响应中增加 `engine_results`
```json
{
"pair_key": "lt1_20250101_20250201_xxxxx",
"task_alias": "Task_20250101_20250201",
"task_pool_dir": "D:\\Task_Pool\\DInSAR\\Task_20250101_20250201",
"engine_results": {
"sarscape": {
"allowed": true,
"status": "missing",
"profile_code": "custom6",
"latest_product_id": null,
"can_dispatch": true,
"skip_reason": null
},
"landsar": {
"allowed": true,
"status": "ready",
"profile_code": "lt1_dinsar",
"latest_product_id": 123,
"can_dispatch": false,
"skip_reason": "result_exists"
},
"pyint": {
"allowed": true,
"status": "failed",
"profile_code": "lt1_gamma_dinsar",
"latest_product_id": 124,
"can_dispatch": true,
"skip_reason": "retry_allowed"
}
}
}
```
状态建议:
- `missing`:没有对应引擎/profile 的有效产品。
- `running`:已有未完成 job 或运行锁。
- `ready`:已有有效产品,默认不重复分发。
- `failed`:最近一次产品或 job 失败,可按策略允许重跑。
- `blocked`:数据类型、轨道、DEM、许可证或输入结构不满足该引擎。
- `legacy`:历史结果存在,但不参与当前三引擎分发规则。
结果矩阵优先从 `result_products` 和运行状态表实时聚合,不建议一开始复制到 batch item 表中。`task_manifest.json` 可以保存发布时快照,但界面展示应以数据库/catalog 聚合结果为准。
## 6. 结果管理重构
当前结果管理按 `result_products` 平铺展示,导致同一任务的三个引擎结果分成三条记录。目标视图应按任务/配对聚合:
```text
Task_20250101_20250201
pair_key: lt1_20250101_20250201_xxxxx
sarscape: ready / latest run / preview / assets
landsar: missing
pyint: ready / latest run / preview / assets
```
后端建议保留 `result_products` 作为原子产品表,新增聚合查询:
- `GET /dinsar-products/pairs`:按 `pair_key``pair_uid``network_edge_id` 聚合结果。
- `GET /dinsar-products/pairs/{pair_key}`:返回同一任务下三个引擎的运行历史、资产、预览和 current 指针。
- `GET /dinsar-products/{product_db_id}`:继续保留单产品详情,作为资产钻取入口。
前端结果管理调整为“一行一个任务/配对”,内部显示三个引擎状态 chip。引擎筛选不再把结果拆成多行,而是控制哪些引擎列/状态参与显示。ISCE2 默认不显示,只能通过 legacy 显式开关查看历史记录。
## 7. 中间文件删除模块
删除能力必须建立在“结果已注册成资产”之后,不能直接按目录名粗暴删除。建议新增 D-InSAR intermediate cleanup 服务,职责如下:
- 扫描某个 `pair_key``run_key``product_db_id` 下可清理路径。
- 校验 `manifest.json``execution_manifest.json``result_assets` 或 manifest assets 中的必要资产都存在。
- 只删除或隔离中间目录,例如 `native/`、engine work dir、临时转换目录、LandSAR 导入过程缓存、PyINT/Gamma 临时处理目录。
- 永远保留标准发布资产:`assets/``preview/``manifest.json``execution_manifest.json``.dinsar_run.json`、current 指针、Task_Pool 任务 manifest 和 `.dinsar_pair.json`
- 支持 `dry_run`,返回将删除的路径、大小和风险原因。
- 第一阶段建议移动到 quarantine/trash,再由运维策略做永久删除。
- 删除动作写审计日志,并把清理状态写回产品摘要或独立 cleanup 表。
建议接口:
- `POST /dinsar-products/{product_db_id}/cleanup-intermediates?dry_run=true`
- `POST /dinsar-products/pairs/{pair_key}/cleanup-intermediates?dry_run=true`
- `GET /dinsar-products/{product_db_id}/cleanup-intermediates/plan`
## 8. 后端改造清单
第一阶段只做读模型和状态聚合,不改变生产执行:
- 新增三引擎常量和能力矩阵,作为后续后端和前端共同事实来源。
- 新增 D-InSAR task item engine result 聚合函数。
- 新增 result catalog pair-grouped 查询。
- 修正 PyINT 按 profile 判断完成状态。
第二阶段收紧 Task_Pool 输入:
- D-InSAR 运行入口只接受 Task_Pool 任务目录或任务目录集合。
- Pairing 发布 batch 时生成或补齐 `task_manifest.json``.dinsar_pair.json`
- 对 LT-1 / Sentinel-1 做引擎可分发校验。
- LandSAR 的 `Input_Data` 生成关系写入 manifest。
第三阶段移除 ISCE2 正式链路:
- 删除 `Isce2Engine` 注册。
- 移除 D-InSAR submit API 中的 `isce2` 分支。
- 移除 ISCE2 job type 到新任务分发的映射。
- 移除 result catalog 新发布中的 ISCE2 特殊处理。
- 前端移除 ISCE2 引擎选项、筛选项和当前说明文案。
- 历史 ISCE2 产品标记为 legacy,只在显式历史入口展示。
第四阶段上线中间文件清理:
- 先提供 dry-run 和 quarantine。
- 加入资产完整性校验。
- 加入审计记录和恢复说明。
- 稳定后再允许永久删除。
## 9. 前端改造清单
- D-InSAR 生产面板只展示 `sarscape``landsar``pyint`
- 数据类型为 Sentinel-1 时,只允许选择 Gamma / PyINTLT-1 时允许三引擎。
- Batch/Task_Pool 列表展示每个任务的三引擎结果状态。
- 已有结果的任务默认显示“已有结果”,并允许按策略重跑或跳过。
- 结果管理从产品平铺改成任务聚合视图。
- 单产品详情保留,用于下载、预览和资产级操作。
- 中间文件清理入口放在产品详情或任务聚合详情内,默认先显示 dry-run 计划。
## 10. 风险与待定项
- 现有 ISCE2 历史结果是否需要迁移到 legacy catalog,还是只隐藏默认入口后保留原记录,需在实际清理前确认。
- ENVI + SARscape 的 `metatask` 是否继续作为可选 profile,还是统一收敛到 `custom6`,需要结合现有项目参数模板决定。
- Task_Pool 根路径需要明确配置项,建议新增 `DINSAR_TASK_POOL_ROOT`,默认 `D:\Task_Pool\DInSAR`
- 中间文件清理需要先确认每个引擎的“可删目录”和“必须保留资产”清单,不能用一套规则覆盖全部。
- 前端 grouped 结果视图需要兼容旧 flat API 一段时间,避免已有页面一次性断裂。
@@ -503,7 +503,7 @@ Git 可以提供平台代码、Python 算法、任务编排、数据库迁移和
The first code pass now implements LT1 and GF3 as analysis-ready scene preprocessors: The first code pass now implements LT1 and GF3 as analysis-ready scene preprocessors:
- New config roots: `SAR_ANALYSIS_READY_ROOT`, `SAR_ANALYSIS_WORK_ROOT`, `SAR_ANALYSIS_PREVIEW_ROOT`. - Current config roots: `SAR_ANALYSIS_READY_ROOT` and `SAR_ANALYSIS_WORK_ROOT`. `SAR_ANALYSIS_PREVIEW_ROOT` was removed; previews live beside each analysis-ready scene.
- `sar_scene_geo` now stores `analysis_tif_path`, `analysis_dir`, preview path, engine/profile, backscatter unit, nodata, metadata and quality JSON. - `sar_scene_geo` now stores `analysis_tif_path`, `analysis_dir`, preview path, engine/profile, backscatter unit, nodata, metadata and quality JSON.
- `/flood/preprocess` routes GF3 scenes to `gf3_gdal` standardization and LT1 scenes to `lt_gamma` single-scene preprocessing. - `/flood/preprocess` routes GF3 scenes to `gf3_gdal` standardization and LT1 scenes to `lt_gamma` single-scene preprocessing.
- GF3 uses the existing GDAL/RPC L1A->L2 result, then registers the selected L2 GeoTIFF under `SAR_ANALYSIS_READY_ROOT`. - GF3 uses the existing GDAL/RPC L1A->L2 result, then registers the selected L2 GeoTIFF under `SAR_ANALYSIS_READY_ROOT`.
@@ -54,7 +54,7 @@
| `pixel_size_m` | 近似像元大小 | | `pixel_size_m` | 近似像元大小 |
| `status` | `PENDING/RUNNING/DONE/FAILED` | | `status` | `PENDING/RUNNING/DONE/FAILED` |
当前 GF3 SARscape 链路会原生 `_geo` ENVI 二进制转换为 `D:\GF3_L2_Image_Pool` 下的 GeoTIFF,并注册到这里。 当前 GF3 SARscape 链路会先产出原生 `_geo` ENVI 二进制,再由平台转换为 `D:\production_results\gf3\standard_l2` 下的 GeoTIFF,并注册到这里。
### 3.2 WaterExtractionORM ### 3.2 WaterExtractionORM
@@ -508,4 +508,3 @@ options: dict | None = None
- 支持模型版本; - 支持模型版本;
- 输出概率图和二值图; - 输出概率图和二值图;
- 与 RF/baseline 可切换。 - 与 RF/baseline 可切换。
@@ -19,12 +19,15 @@ GF3 原始压缩包池
## 2. 目录约定 ## 2. 目录约定
推荐继续沿用现场已有目录语义,并新增一个 ENVI/SARscape 原生池。 推荐把输入、生产结果和运行时目录分开,避免把系统工作文件混入业务结果池。
```env ```env
GF3_ARCHIVE_SOURCE_DIRS=D:\GF3_Image_Pool_Zip GF3_ARCHIVE_SOURCE_DIRS=D:\production_inputs\gf3\archives
GF3_SARSCAPE_NATIVE_DIRS=D:\GF3_L2_ENVI_Binary_Pool GF3_LEGACY_GDAL_ENABLED=false
GF3_STORAGE_DIRS=D:\GF3_L2_Image_Pool GF3_SOURCE_DIRS=
GF3_SARSCAPE_NATIVE_DIRS=D:\production_results\gf3\sarscape_native
GF3_STORAGE_DIRS=D:\production_results\gf3\standard_l2
GF3_SARSCAPE_RUNTIME_DIR=D:\production_runtime\gf3\sarscape_runtime
SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready
``` ```
@@ -33,8 +36,10 @@ SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready
| 目录 | 职责 | 系统是否直接分析 | | 目录 | 职责 | 系统是否直接分析 |
| --- | --- | --- | | --- | --- | --- |
| `GF3_ARCHIVE_SOURCE_DIRS` | 原始 GF3 L1A `.tar.gz` 池 | 否 | | `GF3_ARCHIVE_SOURCE_DIRS` | 原始 GF3 L1A `.tar.gz` 池 | 否 |
| `GF3_SOURCE_DIRS` | legacy Python/GDAL L1A 解包输入,默认关闭 | 否 |
| `GF3_SARSCAPE_NATIVE_DIRS` | SARscape `_geo` 原生结果池 | 否 | | `GF3_SARSCAPE_NATIVE_DIRS` | SARscape `_geo` 原生结果池 | 否 |
| `GF3_STORAGE_DIRS` | GF3 标准 GeoTIFF 池 | 是 | | `GF3_STORAGE_DIRS` | GF3 标准 GeoTIFF 池 | 是 |
| `GF3_SARSCAPE_RUNTIME_DIR` | wrapper 配置、IDL 运行时临时文件 | 否 |
| `SAR_ANALYSIS_READY_ROOT` | 洪涝/水体分析级统一输入 | 是 | | `SAR_ANALYSIS_READY_ROOT` | 洪涝/水体分析级统一输入 | 是 |
生产服务器可以不部署完整管理系统。只要把完成后的 `_geo` 原生结果组放入 `GF3_SARSCAPE_NATIVE_DIRS`,管理系统就可以扫描、转换和入库。 生产服务器可以不部署完整管理系统。只要把完成后的 `_geo` 原生结果组放入 `GF3_SARSCAPE_NATIVE_DIRS`,管理系统就可以扫描、转换和入库。
@@ -44,7 +49,7 @@ SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready
原生池以批次日期或人工批次号分组。单景目录名尽量保持 GF3 原始产品名。 原生池以批次日期或人工批次号分组。单景目录名尽量保持 GF3 原始产品名。
```text ```text
D:\GF3_L2_ENVI_Binary_Pool D:\production_results\gf3\sarscape_native
20260514 20260514
GF3_MH1_FSII_051377_E132.3_N48.2_20260514_L1A_HHHV_L10007356478 GF3_MH1_FSII_051377_E132.3_N48.2_20260514_L1A_HHHV_L10007356478
GF3_MH1_FSII_..._hh_geo GF3_MH1_FSII_..._hh_geo
@@ -110,7 +115,7 @@ SLC 中间产物
系统从原生池转换后写入 `GF3_STORAGE_DIRS` 系统从原生池转换后写入 `GF3_STORAGE_DIRS`
```text ```text
D:\GF3_L2_Image_Pool D:\production_results\gf3\standard_l2
20260514 20260514
GF3_MH1_FSII_051377_E132.3_N48.2_20260514_L1A_HHHV_L10007356478 GF3_MH1_FSII_051377_E132.3_N48.2_20260514_L1A_HHHV_L10007356478
HH_L2.tif HH_L2.tif
@@ -269,8 +274,8 @@ status=DONE
{ {
"schema": "gf3_sarscape_native.v1", "schema": "gf3_sarscape_native.v1",
"scene_name": "GF3_MH1_FSII_...", "scene_name": "GF3_MH1_FSII_...",
"native_dir": "D:\\GF3_L2_ENVI_Binary_Pool\\20260514\\GF3_MH1_FSII_...", "native_dir": "D:\\production_results\\gf3\\sarscape_native\\20260514\\GF3_MH1_FSII_...",
"source_archive": "D:\\GF3_Image_Pool_Zip\\20260514\\GF3_MH1_FSII_....tar.gz", "source_archive": "D:\\production_inputs\\gf3\\archives\\20260514\\GF3_MH1_FSII_....tar.gz",
"polarizations": ["HH", "HV"], "polarizations": ["HH", "HV"],
"status": "NATIVE_READY", "status": "NATIVE_READY",
"assets": [ "assets": [
@@ -295,8 +300,8 @@ status=DONE
{ {
"schema": "gf3_standard_geotiff.v1", "schema": "gf3_standard_geotiff.v1",
"scene_name": "GF3_MH1_FSII_...", "scene_name": "GF3_MH1_FSII_...",
"native_manifest": "D:\\GF3_L2_ENVI_Binary_Pool\\...\\gf3_native_manifest.json", "native_manifest": "D:\\production_results\\gf3\\sarscape_native\\...\\gf3_native_manifest.json",
"standard_dir": "D:\\GF3_L2_Image_Pool\\20260514\\GF3_MH1_FSII_...", "standard_dir": "D:\\production_results\\gf3\\standard_l2\\20260514\\GF3_MH1_FSII_...",
"status": "DONE", "status": "DONE",
"converter": { "converter": {
"name": "gf3_sarscape_geo_to_tif", "name": "gf3_sarscape_geo_to_tif",
@@ -446,7 +451,7 @@ GF3_ARCHIVE_SOURCE_DIRS
新增配置: 新增配置:
```env ```env
GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\.codex_tmp\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\third_party\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe
GF3_SARSCAPE_IDLRT_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe GF3_SARSCAPE_IDLRT_PATH=C:\Program Files\Harris\ENVI56\IDL88\bin\bin.x86_64\idlrt.exe
GF3_SARSCAPE_DEM_PATH=D:\DEM\GMTED2010.jp2 GF3_SARSCAPE_DEM_PATH=D:\DEM\GMTED2010.jp2
GF3_SARSCAPE_POLARIZATIONS=HH,HV GF3_SARSCAPE_POLARIZATIONS=HH,HV
@@ -473,4 +478,4 @@ GF3_SARSCAPE_PRODUCE_TIMEOUT_SECONDS=0
- 每景写 `gf3_cleanup_manifest.json`,记录删除条目和释放字节数。 - 每景写 `gf3_cleanup_manifest.json`,记录删除条目和释放字节数。
- 不删除 `GF3_ARCHIVE_SOURCE_DIRS` 中的原始压缩包,也不删除 `GF3_STORAGE_DIRS` 中的标准 GeoTIFF。 - 不删除 `GF3_ARCHIVE_SOURCE_DIRS` 中的原始压缩包,也不删除 `GF3_STORAGE_DIRS` 中的标准 GeoTIFF。
这样 `D:\GF3_L2_ENVI_Binary_Pool` 只长期保存可追溯的最终 `_geo` 原生结果组,中间过程文件在标准化完成后自动释放空间。 这样 `D:\production_results\gf3\sarscape_native` 只长期保存可追溯的最终 `_geo` 原生结果组,中间过程文件在标准化完成后自动释放空间;wrapper 配置和临时运行文件放在 `D:\production_runtime\gf3\sarscape_runtime`
+10 -5
View File
@@ -1,6 +1,6 @@
# 文档索引 # 文档索引
最后更新:2026-05-30 最后更新:2026-06-14
本页是当前有效文档入口。没有列在本页的历史设计、实验记录和过程文档不再作为当前系统事实依据。 本页是当前有效文档入口。没有列在本页的历史设计、实验记录和过程文档不再作为当前系统事实依据。
@@ -12,6 +12,8 @@
- [DEPLOYMENT.md](DEPLOYMENT.md) - [DEPLOYMENT.md](DEPLOYMENT.md)
Windows + PostgreSQL + WSL2 + Gamma/ISCE2/ENVI 的部署与运行说明。 Windows + PostgreSQL + WSL2 + Gamma/ISCE2/ENVI 的部署与运行说明。
- [BASEMAP_TILESERVER_PROXY_AND_ACCESS_20260613.md](BASEMAP_TILESERVER_PROXY_AND_ACCESS_20260613.md)
Tile-server proxy, LAN access, token configuration, and Nginx IP whitelist.
- [DOCUMENTATION_GOVERNANCE.md](DOCUMENTATION_GOVERNANCE.md) - [DOCUMENTATION_GOVERNANCE.md](DOCUMENTATION_GOVERNANCE.md)
文档治理规则、事实来源优先级和清理约定。 文档治理规则、事实来源优先级和清理约定。
@@ -24,10 +26,13 @@
## 生产与结果 ## 生产与结果
- [PRODUCTION_RESULTS_MULTI_ENGINE_DESIGN_20260423.md](PRODUCTION_RESULTS_MULTI_ENGINE_DESIGN_20260423.md) - [PRODUCTION_RESULTS_MULTI_ENGINE_DESIGN_20260423.md](PRODUCTION_RESULTS_MULTI_ENGINE_DESIGN_20260423.md)
统一结果目录、标准产品包、catalog 与多引擎结果共存约定。 统一结果目录、标准产品包、catalog 与多引擎结果共存约定。D-InSAR 当前引擎集合以 2026-06-14 三引擎 Task_Pool 重构设计为准。
- [DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md](DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md)
D-InSAR 保留 ENVI/SARscape、LandSAR、Gamma/PyINT 三引擎,退出 ISCE2,统一 Task_Pool、结果聚合和中间文件清理的当前设计。
- [DINSAR_PRODUCTION_CORES_OVERVIEW.md](DINSAR_PRODUCTION_CORES_OVERVIEW.md) - [DINSAR_PRODUCTION_CORES_OVERVIEW.md](DINSAR_PRODUCTION_CORES_OVERVIEW.md)
ENVI/SARscape、ISCE2、Gamma/PyINT 三条 D-InSAR 生产核心说明。 旧版 ENVI/SARscape、ISCE2、Gamma/PyINT D-InSAR 生产核心说明。ISCE2 相关内容仅作历史背景。
- [SBAS_INSAR_CURRENT_WORKFLOW.md](SBAS_INSAR_CURRENT_WORKFLOW.md) - [SBAS_INSAR_CURRENT_WORKFLOW.md](SBAS_INSAR_CURRENT_WORKFLOW.md)
当前 Gamma SBAS-InSAR 生产、AOI 选栈、结果 catalog、产物和 LOS 符号约定。 当前 Gamma SBAS-InSAR 生产、AOI 选栈、结果 catalog、产物和 LOS 符号约定。
@@ -41,10 +46,10 @@
PROJ / GDAL 配置说明。 PROJ / GDAL 配置说明。
- [ISCE2_MANAGED_DINSAR_IMPLEMENTATION_20260424.md](ISCE2_MANAGED_DINSAR_IMPLEMENTATION_20260424.md) - [ISCE2_MANAGED_DINSAR_IMPLEMENTATION_20260424.md](ISCE2_MANAGED_DINSAR_IMPLEMENTATION_20260424.md)
ISCE2 托管 D-InSAR 落地说明。 ISCE2 托管 D-InSAR 历史落地说明。新 D-InSAR 生产不再采用。
- [ISCE2_PRODUCTION_RELIABILITY_HARDENING_DESIGN_20260424.md](ISCE2_PRODUCTION_RELIABILITY_HARDENING_DESIGN_20260424.md) - [ISCE2_PRODUCTION_RELIABILITY_HARDENING_DESIGN_20260424.md](ISCE2_PRODUCTION_RELIABILITY_HARDENING_DESIGN_20260424.md)
ISCE2 生产链路稳定性约束。 ISCE2 生产链路历史稳定性约束。新 D-InSAR 生产不再采用。
## 数据与业务模块 ## 数据与业务模块
@@ -2,6 +2,8 @@
更新日期:2026-04-24 更新日期:2026-04-24
> 2026-06-14 状态说明:本文保留结果包、catalog、manifest、current 指针等通用目录约定;D-InSAR 当前引擎集合已调整为 ENVI/SARscape、LandSAR、Gamma/PyINT 三类,ISCE2 退出正式生产链路。D-InSAR 引擎、Task_Pool、结果聚合和中间文件清理以 `DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md` 为准。
## 1. 目标 ## 1. 目标
本设计解决三个长期问题: 本设计解决三个长期问题:
+7 -1
View File
@@ -66,12 +66,13 @@ GAMMA_SBAS_DEFAULT_MB_MODE
GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW
GAMMA_SBAS_STEP_TIMEOUT_SECONDS GAMMA_SBAS_STEP_TIMEOUT_SECONDS
GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS
GAMMA_SBAS_TRIAL_ROOT
``` ```
默认工作根: 默认工作根:
```text ```text
backend/runtime/sbas_insar_production D:\production_runtime\sbas_insar_work
``` ```
默认产品根: 默认产品根:
@@ -80,6 +81,11 @@ backend/runtime/sbas_insar_production
D:\production_results\timeseries\sbas D:\production_results\timeseries\sbas
``` ```
`GAMMA_SBAS_WORK_ROOT` stores heavy, restartable Gamma intermediate data.
`GAMMA_SBAS_PRODUCT_ROOT` stores lightweight catalog/download packages under
`runs/<run_id>`. The result catalog indexes the product root first and keeps the
work root only as a compatibility fallback.
## 4. 生产流程 ## 4. 生产流程
用户侧推荐顺序: 用户侧推荐顺序:
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>frontend</title>
+28 -19
View File
@@ -9,8 +9,10 @@ const DEFAULT_MONITOR_CONFIG = {
dinsar_dirs: [], dinsar_dirs: [],
gf3_archive_source_dirs: [], gf3_archive_source_dirs: [],
gf3_source_dirs: [], gf3_source_dirs: [],
gf3_legacy_gdal_enabled: false,
gf3_sarscape_native_dirs: [], gf3_sarscape_native_dirs: [],
gf3_storage_dirs: [], gf3_storage_dirs: [],
gf3_sarscape_runtime_dir: '',
gf3_sarscape_wrapper_exe: '', gf3_sarscape_wrapper_exe: '',
gf3_sarscape_idlrt_path: '', gf3_sarscape_idlrt_path: '',
gf3_sarscape_dem_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 hasGf3SourceDirs = config.gf3_source_dirs.length > 0;
const hasGf3SarscapeNativeDirs = config.gf3_sarscape_native_dirs.length > 0; const hasGf3SarscapeNativeDirs = config.gf3_sarscape_native_dirs.length > 0;
const hasGf3StorageDirs = config.gf3_storage_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 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() !== ''; 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 canRunS1Scan = !readOnly && configLoaded && hasS1SourceDirs;
const canRunS1OrbitScan = !readOnly && configLoaded && hasS1OrbitDirs; const canRunS1OrbitScan = !readOnly && configLoaded && hasS1OrbitDirs;
const canRunGf3Scan = !readOnly && configLoaded && hasGf3StorageDirs; const canRunGf3Scan = !readOnly && configLoaded && hasGf3StorageDirs;
const canRunGf3Unpack = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SourceDirs; const canRunGf3Unpack = !readOnly && configLoaded && gf3LegacyGdalEnabled && hasGf3ArchiveSourceDirs && hasGf3SourceDirs;
const canRunGf3Process = !readOnly && configLoaded && hasGf3SourceDirs; const canRunGf3Process = !readOnly && configLoaded && gf3LegacyGdalEnabled && hasGf3SourceDirs;
const canRunGf3SarscapeProduce = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SarscapeNativeDirs && hasGf3StorageDirs && hasGf3SarscapeWrapper && hasGf3SarscapeDem; const canRunGf3SarscapeProduce = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SarscapeNativeDirs && hasGf3StorageDirs && hasGf3SarscapeWrapper && hasGf3SarscapeDem;
const canRunGf3SarscapeSync = !readOnly && configLoaded && hasGf3SarscapeNativeDirs && hasGf3StorageDirs; const canRunGf3SarscapeSync = !readOnly && configLoaded && hasGf3SarscapeNativeDirs && hasGf3StorageDirs;
const canRunGf3SarscapeClean = !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_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}>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_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_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 存储</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 style={rowStyle}><span style={labelStyle}>D-InSAR 结果</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.dinsar_dirs)}</span></div>
</div> </div>
</div> </div>
@@ -888,31 +892,36 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
</div> </div>
<div style={sectionStyle}> <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={{ ...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}>压缩包来源</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}>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}>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}>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}>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}>极化</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>
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<button {gf3LegacyGdalEnabled && (
onClick={handleGf3Unpack} <>
disabled={gf3UnpackLoading || readOnly || !canRunGf3Unpack} <button
style={actionBtnStyle(gf3UnpackLoading, readOnly || !canRunGf3Unpack)} onClick={handleGf3Unpack}
> disabled={gf3UnpackLoading || readOnly || !canRunGf3Unpack}
{gf3UnpackLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 解包')} style={actionBtnStyle(gf3UnpackLoading, readOnly || !canRunGf3Unpack)}
</button> >
<button {gf3UnpackLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 legacy 解包')}
onClick={handleGf3BatchProcess} </button>
disabled={gf3ProcessLoading || readOnly || !canRunGf3Process} <button
style={actionBtnStyle(gf3ProcessLoading, readOnly || !canRunGf3Process)} onClick={handleGf3BatchProcess}
> disabled={gf3ProcessLoading || readOnly || !canRunGf3Process}
{gf3ProcessLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 预处理')} style={actionBtnStyle(gf3ProcessLoading, readOnly || !canRunGf3Process)}
</button> >
{gf3ProcessLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 legacy 预处理')}
</button>
</>
)}
<button <button
onClick={handleGf3SarscapeProduce} onClick={handleGf3SarscapeProduce}
disabled={gf3SarscapeProduceLoading || readOnly || !canRunGf3SarscapeProduce} disabled={gf3SarscapeProduceLoading || readOnly || !canRunGf3SarscapeProduce}
+5 -2
View File
@@ -38,8 +38,11 @@ export const TILE_LAYER_OPTIONS = {
maxZoom: 16, maxZoom: 16,
maxNativeZoom: 16, maxNativeZoom: 16,
tms: false, tms: false,
updateWhenIdle: true, updateWhenIdle: false,
keepBuffer: 4, updateWhenZooming: false,
keepBuffer: 2,
reuseTiles: true,
crossOrigin: 'anonymous',
}; };
export const TILE_LAYER_BOUNDS = [ export const TILE_LAYER_BOUNDS = [
+2 -3
View File
@@ -10,8 +10,8 @@ const TASK_UI_POLICIES = {
SCAN_HAZARD: { label: '灾害点同步', featureScope: 'hazard' }, SCAN_HAZARD: { label: '灾害点同步', featureScope: 'hazard' },
UNPACK_ARCHIVES: { label: 'LT-1 解包', featureScope: 'data_monitor' }, UNPACK_ARCHIVES: { label: 'LT-1 解包', featureScope: 'data_monitor' },
UNPACK_SENTINEL1: { label: 'Sentinel-1 解包', featureScope: 'data_monitor' }, UNPACK_SENTINEL1: { label: 'Sentinel-1 解包', featureScope: 'data_monitor' },
GF3_UNPACK: { label: 'GF3 解包', featureScope: 'data_monitor' }, GF3_UNPACK: { label: 'GF3 legacy 解包', featureScope: 'data_monitor' },
GF3_BATCH_PROCESS: { label: 'GF3 预处理', featureScope: 'data_monitor' }, GF3_BATCH_PROCESS: { label: 'GF3 legacy 预处理', featureScope: 'data_monitor' },
GF3_SARSCAPE_PRODUCE: { label: 'GF3 SARscape 生产', featureScope: 'data_monitor' }, GF3_SARSCAPE_PRODUCE: { label: 'GF3 SARscape 生产', featureScope: 'data_monitor' },
GF3_SARSCAPE_SYNC: { label: 'GF3 SARscape 入库', featureScope: 'data_monitor' }, GF3_SARSCAPE_SYNC: { label: 'GF3 SARscape 入库', featureScope: 'data_monitor' },
GF3_SARSCAPE_CLEAN: { label: 'GF3 中间清理', featureScope: 'data_monitor' }, GF3_SARSCAPE_CLEAN: { label: 'GF3 中间清理', featureScope: 'data_monitor' },
@@ -73,4 +73,3 @@ export function isTaskGloballyBlocking(taskType) {
export function getTaskTypeLabel(taskType) { export function getTaskTypeLabel(taskType) {
return getTaskUiPolicy(taskType).label; return getTaskUiPolicy(taskType).label;
} }
+50 -8
View File
@@ -3,6 +3,39 @@ import apiClient from '../api/client';
import { getHealth } from '../api/health'; import { getHealth } from '../api/health';
const HEALTH_POLL_INTERVAL_MS = 30000; 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({ export default function useAppAuthLifecycle({
ensureCanOperate, ensureCanOperate,
@@ -33,15 +66,21 @@ export default function useAppAuthLifecycle({
const { clearOnFailure = true } = options; const { clearOnFailure = true } = options;
const requestGeneration = authGenerationRef.current; const requestGeneration = authGenerationRef.current;
try { try {
const response = await apiClient.get('/auth/me', { const response = await requestWithTransientRetry(() => (
skipAuthReset: !clearOnFailure, apiClient.get('/auth/me', {
authGeneration: requestGeneration, skipAuthReset: !clearOnFailure,
}); authGeneration: requestGeneration,
})
));
if (requestGeneration === authGenerationRef.current) { if (requestGeneration === authGenerationRef.current) {
setCurrentUser(response.data || null); setCurrentUser(response.data || null);
} }
} catch { } catch (error) {
if (clearOnFailure && requestGeneration === authGenerationRef.current) { if (
clearOnFailure
&& getErrorStatus(error) === 401
&& requestGeneration === authGenerationRef.current
) {
setCurrentUser(null); setCurrentUser(null);
} }
} finally { } finally {
@@ -88,7 +127,7 @@ export default function useAppAuthLifecycle({
const fetchLicenseStatus = useCallback(async () => { const fetchLicenseStatus = useCallback(async () => {
try { try {
setLicenseLoading(true); setLicenseLoading(true);
const response = await apiClient.get('/license/status'); const response = await requestWithTransientRetry(() => apiClient.get('/license/status'));
const data = response.data || {}; const data = response.data || {};
if (!data.ok && !data.reason) { if (!data.ok && !data.reason) {
data.reason = '未授权'; data.reason = '未授权';
@@ -111,7 +150,10 @@ export default function useAppAuthLifecycle({
setHealthLoading(true); setHealthLoading(true);
} }
setHealthError(''); 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); setHealthStatus(data || null);
} catch (error) { } catch (error) {
setHealthError(error.response?.data?.detail || '运维自检失败'); setHealthError(error.response?.data?.detail || '运维自检失败');
+94
View File
@@ -0,0 +1,94 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Access Restricted</title>
<style>
:root {
color-scheme: light;
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
background: #f6f7fb;
color: #182033;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px;
}
main {
width: min(720px, 100%);
background: #ffffff;
border: 1px solid #d9dfeb;
border-radius: 8px;
box-shadow: 0 18px 48px rgba(23, 31, 50, 0.10);
padding: 34px;
}
.eyebrow {
margin: 0 0 12px;
color: #7f1d1d;
font-size: 13px;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
h1 {
margin: 0 0 14px;
font-size: 30px;
line-height: 1.18;
}
p {
margin: 0 0 18px;
color: #475569;
line-height: 1.7;
font-size: 15px;
}
dl {
display: grid;
grid-template-columns: 120px 1fr;
gap: 10px 14px;
margin: 24px 0;
padding: 18px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
dt {
color: #64748b;
font-weight: 700;
}
dd {
margin: 0;
font-family: Consolas, "SFMono-Regular", monospace;
color: #0f172a;
overflow-wrap: anywhere;
}
.notice {
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid #e2e8f0;
font-size: 14px;
color: #64748b;
}
</style>
</head>
<body>
<main>
<p class="eyebrow">403 Access Restricted</p>
<h1>当前设备未被授权访问 InSAR 管理系统</h1>
<p>系统仅允许预先登记的局域网 IP 访问。请将下面的客户端 IP 提供给管理员,由管理员加入访问白名单后再重试。</p>
<dl>
<dt>客户端 IP</dt>
<dd>__CLIENT_IP__</dd>
<dt>访问主机</dt>
<dd>__REQUEST_HOST__</dd>
</dl>
<p class="notice">管理员配置入口:项目根目录 <strong>.env</strong> 中的 <strong>NGINX_ALLOWED_CLIENT_IPS</strong>,修改后重启或 reload Nginx。</p>
</main>
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
# Generated by scripts/start_app.ps1.
# Configure NGINX_ALLOWED_CLIENT_IPS in .env. Empty means allow all clients.
+68 -3
View File
@@ -24,18 +24,44 @@ http {
server { server {
listen 80; listen 80;
server_name localhost; server_name localhost;
include "D:/Code/Insar_management_system_v2/nginx/client_allow.conf";
error_page 403 /__access_denied.html;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# CSP: allow both legacy tile port 8900 and current tile-server port 8910 add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; 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:;" always;
# If tile server address changes, update the sources below accordingly
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; 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:;" always;
root "D:/Code/Insar_management_system_v2/frontend/dist"; root "D:/Code/Insar_management_system_v2/frontend/dist";
index index.html; index index.html;
location = /index.html {
try_files /index.html =404;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}
location /assets/ {
try_files $uri @asset_not_found;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location @asset_not_found {
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
return 404;
}
location = /__access_denied.html {
internal;
allow all;
root "D:/Code/Insar_management_system_v2/frontend/dist";
try_files /access_denied.html =403;
default_type text/html;
sub_filter_once off;
sub_filter "__CLIENT_IP__" "$remote_addr";
sub_filter "__REQUEST_HOST__" "$host";
}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
@@ -49,6 +75,45 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location /tiles/ {
proxy_pass http://127.0.0.1:8910;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_valid 200 365d;
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location /geojson/ {
proxy_pass http://127.0.0.1:8910;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /shp/ {
proxy_pass http://127.0.0.1:8910;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /vectors/ {
proxy_pass http://127.0.0.1:8910;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# SSE: disable buffering so events are pushed immediately # SSE: disable buffering so events are pushed immediately
location /api/tasks/active/stream { location /api/tasks/active/stream {
proxy_pass http://127.0.0.1:18000; proxy_pass http://127.0.0.1:18000;
+374
View File
@@ -0,0 +1,374 @@
"""Inspect and optionally clean generated runtime data.
The script is deliberately conservative:
- dry-run is the default;
- protected paths are collected from database product/artifact references;
- project source, local software, external data pools, and license files are never touched.
"""
from __future__ import annotations
import argparse
import asyncio
import os
import shutil
import sys
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from typing import Iterable
PROJECT_ROOT = Path(__file__).resolve().parents[1]
BACKEND_DIR = PROJECT_ROOT / "backend"
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from backend.app.config import ensure_project_env_loaded, read_int_env, settings # noqa: E402
ACTIVE_STATUSES = {"PENDING", "READY", "RUNNING", "PROCESSING", "IN_PROGRESS", "QUEUED", "RETRY"}
@dataclass(frozen=True)
class CleanupRoot:
label: str
path: Path
default_retention_days: int
mode: str = "children"
protect_db_references: bool = False
enabled_by_default: bool = True
@dataclass
class Candidate:
label: str
path: Path
reason: str
size_bytes: int
files: int
dirs: int
last_write: datetime | None
protected: bool = False
deleted: bool = False
error: str = ""
def _norm(path: Path | str) -> Path:
return Path(path).expanduser().resolve()
def _is_under(path: Path, parent: Path) -> bool:
try:
path.relative_to(parent)
return True
except ValueError:
return False
def _parse_dt(value: object) -> datetime | None:
if isinstance(value, datetime):
return value
text = str(value or "").strip()
if not text:
return None
for fmt in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
try:
return datetime.strptime(text[:26], fmt)
except ValueError:
continue
return None
def _measure(path: Path) -> tuple[int, int, int, datetime | None]:
if not path.exists():
return 0, 0, 0, None
if path.is_file():
stat = path.stat()
return stat.st_size, 1, 0, datetime.fromtimestamp(stat.st_mtime)
size = 0
files = 0
dirs = 0
last_write = datetime.fromtimestamp(path.stat().st_mtime)
for entry in path.rglob("*"):
try:
stat = entry.stat()
except OSError:
continue
if entry.is_dir():
dirs += 1
else:
files += 1
size += stat.st_size
mtime = datetime.fromtimestamp(stat.st_mtime)
if mtime > last_write:
last_write = mtime
return size, files, dirs, last_write
def _format_gb(size_bytes: int) -> str:
return f"{size_bytes / (1024 ** 3):.3f} GB"
def _candidate_children(root: CleanupRoot, cutoff: datetime) -> list[Candidate]:
path = _norm(root.path)
if not path.exists() or not path.is_dir():
return []
candidates: list[Candidate] = []
for child in path.iterdir():
size, files, dirs, last_write = _measure(child)
if last_write and last_write > cutoff:
continue
candidates.append(
Candidate(
label=root.label,
path=_norm(child),
reason=f"last_write <= {cutoff.isoformat(timespec='seconds')}",
size_bytes=size,
files=files,
dirs=dirs,
last_write=last_write,
)
)
return candidates
def _candidate_root(root: CleanupRoot, cutoff: datetime) -> list[Candidate]:
path = _norm(root.path)
if not path.exists():
return []
size, files, dirs, last_write = _measure(path)
if last_write and last_write > cutoff:
return []
return [
Candidate(
label=root.label,
path=path,
reason=f"root last_write <= {cutoff.isoformat(timespec='seconds')}",
size_bytes=size,
files=files,
dirs=dirs,
last_write=last_write,
)
]
def _configured_roots() -> list[CleanupRoot]:
runtime_dir = BACKEND_DIR / "runtime"
return [
CleanupRoot(
"pyint_work",
Path(settings.PYINT_WORK_ROOT or runtime_dir / "pyint_work"),
read_int_env("RUNTIME_CLEANUP_PYINT_WORK_RETENTION_DAYS", 14),
),
CleanupRoot(
"pyint_dem",
Path(settings.PYINT_DEM_ROOT or runtime_dir / "pyint_dem"),
read_int_env("RUNTIME_CLEANUP_PYINT_DEM_RETENTION_DAYS", 60),
),
CleanupRoot(
"sbas_insar_production_runs",
Path(settings.GAMMA_SBAS_WORK_ROOT or runtime_dir / "sbas_insar_production") / "runs",
read_int_env("RUNTIME_CLEANUP_SBAS_RUN_RETENTION_DAYS", 30),
protect_db_references=True,
),
CleanupRoot(
"gamma_ipta_trials",
Path(settings.GAMMA_SBAS_TRIAL_ROOT or runtime_dir / "gamma_ipta_trials"),
read_int_env("RUNTIME_CLEANUP_TRIAL_RETENTION_DAYS", 7),
),
CleanupRoot(
"idl_worker_logs",
Path(settings.IDL_WORKER_RUNTIME_DIR or runtime_dir / "idl_worker"),
read_int_env("RUNTIME_CLEANUP_IDL_LOG_RETENTION_DAYS", 30),
),
CleanupRoot(
"wsl_jobs",
Path(settings.WSL_BROKER_JOB_ROOT or runtime_dir / "wsl_jobs"),
read_int_env("RUNTIME_CLEANUP_WSL_JOB_RETENTION_DAYS", 14),
),
CleanupRoot(
"timeseries_work",
Path(settings.TIMESERIES_WORK_ROOT or runtime_dir / "timeseries_work"),
read_int_env("RUNTIME_CLEANUP_TIMESERIES_WORK_RETENTION_DAYS", 30),
),
CleanupRoot(
"image_cache",
Path(settings.CACHE_DIR),
read_int_env("RUNTIME_CLEANUP_IMAGE_CACHE_RETENTION_DAYS", 60),
),
CleanupRoot(
"frontend_dist",
PROJECT_ROOT / "frontend" / "dist",
read_int_env("RUNTIME_CLEANUP_FRONTEND_DIST_RETENTION_DAYS", 0),
mode="root",
enabled_by_default=False,
),
CleanupRoot(
"logs",
PROJECT_ROOT / "logs",
read_int_env("RUNTIME_CLEANUP_LOG_RETENTION_DAYS", 30),
),
CleanupRoot(
"nginx_temp",
PROJECT_ROOT / "nginx" / "temp",
0,
),
]
async def _db_rows(query: str) -> list[tuple]:
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
url = str(settings.DATABASE_URL or "").strip()
if not url:
return []
engine = create_async_engine(url, pool_pre_ping=True)
try:
async with engine.connect() as conn:
result = await conn.execute(text(query))
return list(result.all())
finally:
await engine.dispose()
async def _active_status_summary() -> dict[str, int]:
queries = {
"system_jobs": "select count(*) from system_jobs where upper(status) in ('PENDING','READY','RUNNING','PROCESSING','IN_PROGRESS','QUEUED','RETRY')",
"system_tasks": "select count(*) from system_tasks where upper(status) in ('PENDING','RUNNING','PROCESSING','IN_PROGRESS','QUEUED')",
"workflow_runs": "select count(*) from workflow_runs where upper(status) in ('PENDING','RUNNING','PROCESSING','IN_PROGRESS','QUEUED')",
}
summary: dict[str, int] = {}
for key, query in queries.items():
try:
rows = await _db_rows(query)
summary[key] = int(rows[0][0] or 0) if rows else 0
except Exception:
summary[key] = -1
return summary
async def _protected_paths() -> set[Path]:
protected: set[Path] = set()
queries = [
"select publish_dir, manifest_path, source_primary_path, native_output_dir, preview_path, primary_asset_path from result_products",
"select absolute_path from result_assets",
"select path from workflow_artifacts",
"select storage_root from result_catalog_states",
]
for query in queries:
try:
rows = await _db_rows(query)
except Exception as exc:
print(f"[WARN] Could not read protected paths: {exc}")
continue
for row in rows:
for value in row:
text = str(value or "").strip()
if not text:
continue
try:
protected.add(_norm(text))
except OSError:
continue
return protected
def _apply_protection(candidates: Iterable[Candidate], protected: set[Path]) -> list[Candidate]:
output: list[Candidate] = []
for candidate in candidates:
path = candidate.path
for protected_path in protected:
if path == protected_path or _is_under(protected_path, path) or _is_under(path, protected_path):
candidate.protected = True
candidate.reason = f"database reference protects {protected_path}"
break
output.append(candidate)
return output
def _delete(candidate: Candidate) -> None:
try:
if candidate.path.is_dir():
shutil.rmtree(candidate.path)
else:
candidate.path.unlink()
candidate.deleted = True
except Exception as exc:
candidate.error = f"{type(exc).__name__}: {exc}"
async def collect_candidates(include_optional: bool, include_external_runtime: bool) -> tuple[list[Candidate], dict[str, int]]:
ensure_project_env_loaded()
now = datetime.now()
candidates: list[Candidate] = []
for root in _configured_roots():
if not root.enabled_by_default and not include_optional:
continue
root_path = _norm(root.path)
if not _is_under(root_path, PROJECT_ROOT):
if not include_external_runtime:
print(f"[SKIP] {root.label}: outside project root: {root_path}")
continue
cutoff = now - timedelta(days=max(0, int(root.default_retention_days)))
if root.mode == "root":
items = _candidate_root(root, cutoff)
else:
items = _candidate_children(root, cutoff)
candidates.extend(items)
protected = await _protected_paths()
candidates = _apply_protection(candidates, protected)
active_summary = await _active_status_summary()
if any(value > 0 for value in active_summary.values()):
for candidate in candidates:
if candidate.label in {"pyint_work", "sbas_insar_production_runs", "wsl_jobs", "timeseries_work"}:
candidate.protected = True
candidate.reason = f"active database state present: {active_summary}"
return candidates, active_summary
def print_report(candidates: list[Candidate], active_summary: dict[str, int]) -> None:
total = sum(item.size_bytes for item in candidates)
deletable = sum(item.size_bytes for item in candidates if not item.protected)
print(f"[INFO] Active DB state: {active_summary}")
print(f"[INFO] Candidates: {len(candidates)}, total={_format_gb(total)}, deletable={_format_gb(deletable)}")
for item in sorted(candidates, key=lambda x: x.size_bytes, reverse=True):
status = "PROTECTED" if item.protected else ("DELETED" if item.deleted else "DELETE_CANDIDATE")
last_write = item.last_write.isoformat(timespec="seconds") if item.last_write else "unknown"
print(
f"{status}\t{item.label}\t{_format_gb(item.size_bytes)}\t"
f"files={item.files}\tdirs={item.dirs}\tlast={last_write}\t{item.path}\t{item.reason}"
)
if item.error:
print(f" ERROR: {item.error}")
async def main() -> int:
parser = argparse.ArgumentParser(description="Inspect and clean generated runtime data.")
parser.add_argument("--delete", action="store_true", help="Actually delete unprotected candidates.")
parser.add_argument("--include-optional", action="store_true", help="Include optional targets such as frontend/dist.")
parser.add_argument("--include-external-runtime", action="store_true", help="Also inspect configured runtime roots outside the project.")
parser.add_argument("--yes", action="store_true", help="Required together with --delete.")
args = parser.parse_args()
candidates, active_summary = await collect_candidates(args.include_optional, args.include_external_runtime)
if args.delete and not args.yes:
print("[ERROR] --delete requires --yes")
print_report(candidates, active_summary)
return 2
if args.delete:
for candidate in candidates:
if candidate.protected:
continue
_delete(candidate)
print_report(candidates, active_summary)
return 0
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))
+175
View File
@@ -0,0 +1,175 @@
"""Migrate repo-local runtime data to configured external roots."""
from __future__ import annotations
import argparse
import asyncio
import shutil
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
BACKEND_DIR = PROJECT_ROOT / "backend"
LEGACY_RUNTIME = BACKEND_DIR / "runtime"
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from backend.app.config import settings # noqa: E402
from backend.app import database # noqa: E402
from backend.app.services.sbas_insar_catalog_service import sbas_insar_catalog_service # noqa: E402
from backend.app.services.sbas_insar_production_service import sbas_insar_production_service # noqa: E402
def _norm(path: Path | str) -> Path:
return Path(path).expanduser().resolve()
def _is_under(path: Path, parent: Path) -> bool:
try:
path.relative_to(parent)
return True
except ValueError:
return False
def _measure(path: Path) -> tuple[int, int]:
if not path.exists():
return 0, 0
if path.is_file():
return path.stat().st_size, 1
size = 0
files = 0
for item in path.rglob("*"):
if item.is_file():
try:
size += item.stat().st_size
files += 1
except OSError:
continue
return size, files
def _gb(size: int) -> str:
return f"{size / (1024 ** 3):.3f} GB"
def _copy_tree(source: Path, target: Path, *, delete_source: bool) -> dict[str, object]:
if not source.exists():
return {"source": str(source), "target": str(target), "exists": False, "copied": False}
if _norm(target) == _norm(source):
return {"source": str(source), "target": str(target), "exists": True, "copied": False, "reason": "same_path"}
if _is_under(_norm(target), _norm(source)):
raise ValueError(f"refusing to copy {source} into itself: {target}")
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
raise FileExistsError(f"target already exists: {target}")
shutil.copytree(source, target)
size, files = _measure(target)
if delete_source:
shutil.rmtree(source)
return {
"source": str(source),
"target": str(target),
"exists": True,
"copied": True,
"deleted_source": bool(delete_source),
"size": _gb(size),
"files": files,
}
def migrate_pyint_dem(*, apply: bool, delete_source: bool) -> dict[str, object]:
source = LEGACY_RUNTIME / "pyint_dem"
target = Path(settings.PYINT_DEM_ROOT)
size, files = _measure(source)
plan = {
"source": str(source),
"target": str(target),
"source_exists": source.exists(),
"source_size": _gb(size),
"source_files": files,
"apply": apply,
"delete_source": delete_source,
}
if not apply:
return plan
return {**plan, "result": _copy_tree(source, target, delete_source=delete_source)}
def sync_sbas_products(*, apply: bool) -> dict[str, object]:
legacy_root = LEGACY_RUNTIME / "sbas_insar_production" / "runs"
configured_work_root = Path(settings.GAMMA_SBAS_WORK_ROOT) / "runs"
roots = []
for root in (legacy_root, configured_work_root):
if root.is_dir() and root not in roots:
roots.append(root)
run_ids = sorted(
{
path.parent.name
for root in roots
for path in root.glob("*/run_manifest.json")
}
)
plan: dict[str, object] = {
"source_roots": [str(root) for root in roots],
"product_root": str(Path(settings.GAMMA_SBAS_PRODUCT_ROOT) / "runs"),
"run_ids": run_ids,
"apply": apply,
}
if not apply:
return plan
synced = []
original_root = sbas_insar_production_service.production_root
try:
for run_id in run_ids:
selected_root = next((root for root in roots if (root / run_id / "run_manifest.json").is_file()), None)
if selected_root is None:
continue
sbas_insar_production_service.production_root = selected_root.parent
synced.append(sbas_insar_production_service.sync_product_package(run_id))
finally:
sbas_insar_production_service.production_root = original_root
plan["synced"] = synced
return plan
async def rebuild_sbas_catalog() -> dict[str, object]:
if database.AsyncSessionLocal is None:
database.init_db(settings.DATABASE_URL)
if database.AsyncSessionLocal is None:
raise RuntimeError("Database session factory is not initialized")
async with database.AsyncSessionLocal() as db:
return await sbas_insar_catalog_service.rebuild_catalog(db, full_rebuild=True)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--apply", action="store_true", help="perform the migration; default only prints the plan")
parser.add_argument("--move-pyint-dem", action="store_true", help="copy legacy pyint_dem to PYINT_DEM_ROOT")
parser.add_argument("--delete-legacy-pyint-dem", action="store_true", help="delete legacy pyint_dem after copy")
parser.add_argument("--sync-sbas-products", action="store_true", help="sync lightweight SBAS products into GAMMA_SBAS_PRODUCT_ROOT")
parser.add_argument("--rebuild-sbas-catalog", action="store_true", help="rebuild SBAS result catalog after syncing products")
args = parser.parse_args()
actions: dict[str, object] = {}
if args.move_pyint_dem:
actions["pyint_dem"] = migrate_pyint_dem(apply=args.apply, delete_source=args.delete_legacy_pyint_dem)
if args.sync_sbas_products:
actions["sbas_products"] = sync_sbas_products(apply=args.apply)
if args.rebuild_sbas_catalog:
if args.apply:
actions["sbas_catalog"] = asyncio.run(rebuild_sbas_catalog())
else:
actions["sbas_catalog"] = {"apply": False, "action": "rebuild_sbas_catalog"}
if not actions:
actions["message"] = "no actions selected"
import json
print(json.dumps(actions, indent=2, ensure_ascii=False, default=str))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+207 -1
View File
@@ -29,6 +29,14 @@ $CondaEnvName = ""
$NginxExe = "C:/nginx-1.29.4/nginx.exe" $NginxExe = "C:/nginx-1.29.4/nginx.exe"
$ServerHost = "" $ServerHost = ""
$ServerPort = 18000 $ServerPort = 18000
$NginxAllowedClientIps = ""
$BackendReadyTimeoutSeconds = 120
$TileServerAutoStart = $false
$TileServerAutoStop = $true
$TileServerRoot = ""
$TileServerStartScript = "start-all.bat"
$TileServerStopScript = "stop-all.bat"
$TileServerUrl = ""
$envLines = Get-Content -LiteralPath "$EnvPath" $envLines = Get-Content -LiteralPath "$EnvPath"
foreach ($line in $envLines) { foreach ($line in $envLines) {
@@ -43,7 +51,18 @@ foreach ($line in $envLines) {
if ($key -eq "CONDA_EXE") { if ($val) { $CondaExe = $val } } if ($key -eq "CONDA_EXE") { if ($val) { $CondaExe = $val } }
if ($key -eq "CONDA_ENV_NAME") { if ($val) { $CondaEnvName = $val } } if ($key -eq "CONDA_ENV_NAME") { if ($val) { $CondaEnvName = $val } }
if ($key -eq "NGINX_PATH") { if ($val) { $NginxExe = $val } } if ($key -eq "NGINX_PATH") { if ($val) { $NginxExe = $val } }
if ($key -eq "NGINX_ALLOWED_CLIENT_IPS") { $NginxAllowedClientIps = $val }
if ($key -eq "BACKEND_BIND_HOST") { if ($val) { $ServerHost = $val } } if ($key -eq "BACKEND_BIND_HOST") { if ($val) { $ServerHost = $val } }
if ($key -eq "BACKEND_READY_TIMEOUT_SECONDS") {
$parsed = 0
if ([int]::TryParse($val, [ref]$parsed) -and $parsed -gt 0) { $BackendReadyTimeoutSeconds = $parsed }
}
if ($key -eq "TILE_SERVER_AUTO_START") { $TileServerAutoStart = $val -match '^(?i)(true|1|yes|on)$' }
if ($key -eq "TILE_SERVER_AUTO_STOP") { $TileServerAutoStop = -not ($val -match '^(?i)(false|0|no|off)$') }
if ($key -eq "TILE_SERVER_ROOT") { if ($val) { $TileServerRoot = $val } }
if ($key -eq "TILE_SERVER_START_SCRIPT") { if ($val) { $TileServerStartScript = $val } }
if ($key -eq "TILE_SERVER_STOP_SCRIPT") { if ($val) { $TileServerStopScript = $val } }
if ($key -eq "VITE_TILE_SERVER_URL") { if ($val) { $TileServerUrl = $val.TrimEnd("/") } }
if ($key -eq "PORT") { if ($key -eq "PORT") {
$parsed = 0 $parsed = 0
if ([int]::TryParse($val, [ref]$parsed)) { $ServerPort = $parsed } if ([int]::TryParse($val, [ref]$parsed)) { $ServerPort = $parsed }
@@ -315,11 +334,140 @@ function Test-PortAvailable {
} }
} }
function Write-NginxClientAllowFile {
param([string]$Path)
$entries = @()
$raw = "$NginxAllowedClientIps".Trim()
if ($raw) {
$entries = @($raw -split '[;,\s]+' | ForEach-Object { "$_".Trim() } | Where-Object { $_ })
}
$lines = @(
"# Generated by scripts/start_app.ps1.",
"# Configure NGINX_ALLOWED_CLIENT_IPS in .env. Empty means allow all clients."
)
if ($entries.Count -gt 0) {
$allowed = @("127.0.0.1", "::1") + $entries
$allowed = @($allowed | Sort-Object -Unique)
foreach ($item in $allowed) {
$lines += " allow $item;"
}
$lines += " deny all;"
}
$dir = Split-Path -Parent "$Path"
if (-not (Test-Path -LiteralPath "$dir")) {
New-Item -ItemType Directory -Path "$dir" -Force | Out-Null
}
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText("$Path", (($lines -join [Environment]::NewLine) + [Environment]::NewLine), $Utf8NoBom)
if ($entries.Count -gt 0) {
Write-Host ">>> Nginx client IP whitelist enabled: $($entries -join ', ')" -ForegroundColor Yellow
} else {
Write-Host ">>> Nginx client IP whitelist disabled." -ForegroundColor DarkGray
}
}
function Test-TileServerReady {
if (-not $TileServerUrl) {
return $false
}
try {
$response = Invoke-WebRequest -Uri "$TileServerUrl/health" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
return ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500)
} catch {
return $false
}
}
function Get-TileServerScriptPath {
param([string]$ScriptName)
$root = "$TileServerRoot".Trim().Trim('"').Trim("'")
if (-not $root) {
return $null
}
$script = "$ScriptName".Trim().Trim('"').Trim("'")
if (-not $script) {
return $null
}
if ([System.IO.Path]::IsPathRooted($script)) {
return $script
}
return (Join-Path -Path $root -ChildPath $script)
}
function Stop-TileServer {
if (-not $TileServerAutoStop) {
return
}
$stopScript = Get-TileServerScriptPath -ScriptName "$TileServerStopScript"
if (-not $stopScript -or -not (Test-Path -LiteralPath "$stopScript")) {
return
}
Write-Host ">>> Stopping tile-server..." -ForegroundColor Yellow
$previousNoPause = $env:NO_PAUSE
try {
$env:NO_PAUSE = "1"
& "$stopScript"
} catch {
Write-Warning "tile-server stop failed: $($_.Exception.Message)"
} finally {
if ($null -eq $previousNoPause) {
Remove-Item Env:\NO_PAUSE -ErrorAction SilentlyContinue
} else {
$env:NO_PAUSE = $previousNoPause
}
}
}
function Start-TileServer {
if (-not $TileServerAutoStart) {
return
}
if (Test-TileServerReady) {
Write-Host ">>> tile-server already responding: $TileServerUrl" -ForegroundColor Green
return
}
$startScript = Get-TileServerScriptPath -ScriptName "$TileServerStartScript"
if (-not $startScript -or -not (Test-Path -LiteralPath "$startScript")) {
Write-Error "tile-server start script not found. TILE_SERVER_ROOT=$TileServerRoot TILE_SERVER_START_SCRIPT=$TileServerStartScript"
$global:LASTEXITCODE = 1
return
}
Write-Host ">>> Launching tile-server..." -ForegroundColor Green
$previousNoPause = $env:NO_PAUSE
try {
$env:NO_PAUSE = "1"
& "$startScript"
} finally {
if ($null -eq $previousNoPause) {
Remove-Item Env:\NO_PAUSE -ErrorAction SilentlyContinue
} else {
$env:NO_PAUSE = $previousNoPause
}
}
for ($i = 0; $i -lt 20; $i++) {
if (Test-TileServerReady) {
Write-Host ">>> tile-server ready: $TileServerUrl" -ForegroundColor Green
return
}
Start-Sleep -Milliseconds 500
}
Write-Warning "tile-server was started but did not pass health check: $TileServerUrl/health"
}
Stop-Backend-By-Cmdline -MatchText "run_backend.py" Stop-Backend-By-Cmdline -MatchText "run_backend.py"
Stop-Backend-By-Cmdline -MatchText "run_worker.py" Stop-Backend-By-Cmdline -MatchText "run_worker.py"
$NginxProcName = Split-Path -Leaf $NginxExe $NginxProcName = Split-Path -Leaf $NginxExe
$NginxProcName = $NginxProcName -replace '\.exe$', '' $NginxProcName = $NginxProcName -replace '\.exe$', ''
Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe
if ($TileServerAutoStart -and $TileServerAutoStop) {
Stop-TileServer
}
$PortAvailable = Test-PortAvailable -Port $ServerPort $PortAvailable = Test-PortAvailable -Port $ServerPort
if (-not $PortAvailable) { if (-not $PortAvailable) {
@@ -455,6 +603,38 @@ function Assert-ProcessAlive {
return $true return $true
} }
function Wait-BackendReady {
param(
[int]$Port,
[int]$TimeoutSeconds
)
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $TimeoutSeconds))
# Use the lightweight root route for readiness. /api/health runs a full
# operational self-check and can legitimately take longer during startup.
$url = "http://127.0.0.1:$Port/"
$lastError = $null
Write-Host ">>> Waiting for backend readiness: $url" -ForegroundColor Yellow
while ((Get-Date) -lt $deadline) {
try {
$response = Invoke-WebRequest -Uri "$url" -UseBasicParsing -TimeoutSec 3 -ErrorAction Stop
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
Write-Host ">>> Backend ready (status=$($response.StatusCode))." -ForegroundColor Green
return $true
}
$lastError = "HTTP $($response.StatusCode)"
} catch {
$lastError = $_.Exception.Message
}
Start-Sleep -Milliseconds 800
}
Write-Error "Backend did not become ready within $TimeoutSeconds seconds. Last error: $lastError"
$global:LASTEXITCODE = 1
return $false
}
Invoke-PythonScript -ScriptPath "$CheckRuntimeScript" Invoke-PythonScript -ScriptPath "$CheckRuntimeScript"
if ($LastExitCode -ne 0) { if ($LastExitCode -ne 0) {
Write-Host "`n[ERROR] Deployment configuration check failed." -ForegroundColor Red Write-Host "`n[ERROR] Deployment configuration check failed." -ForegroundColor Red
@@ -506,16 +686,29 @@ if (Test-Path -LiteralPath "$NginxConfPath") {
$ForwardRoot = $ProjectRoot.Replace([char]92, [char]47) $ForwardRoot = $ProjectRoot.Replace([char]92, [char]47)
$FrontendDist = "$ForwardRoot/frontend/dist" $FrontendDist = "$ForwardRoot/frontend/dist"
$ImageCache = "$ForwardRoot/backend/image_cache" $ImageCache = "$ForwardRoot/backend/image_cache"
$NginxClientAllowFile = Join-Path -Path $ProjectRoot -ChildPath "nginx\client_allow.conf"
if (-not (Test-Path -LiteralPath "$ProjectRoot/backend/image_cache")) { if (-not (Test-Path -LiteralPath "$ProjectRoot/backend/image_cache")) {
New-Item -ItemType Directory -Path "$ProjectRoot/backend/image_cache" -Force | Out-Null New-Item -ItemType Directory -Path "$ProjectRoot/backend/image_cache" -Force | Out-Null
} }
Write-NginxClientAllowFile -Path "$NginxClientAllowFile"
$ConfContent = Get-Content -LiteralPath "$NginxConfPath" -Raw $ConfContent = Get-Content -LiteralPath "$NginxConfPath" -Raw
$NewConfContent = $ConfContent -replace 'root\s+[^;]+;', "root `"$FrontendDist`";" $NewConfContent = $ConfContent -replace 'root\s+[^;]+;', "root `"$FrontendDist`";"
$NewConfContent = $NewConfContent -replace 'alias\s+[^;]+;', "alias `"$ImageCache/`";" $NewConfContent = $NewConfContent -replace 'alias\s+[^;]+;', "alias `"$ImageCache/`";"
$ClientAllowForwardPath = "$ForwardRoot/nginx/client_allow.conf"
$NewConfContent = $NewConfContent -replace 'include\s+"[^"]*client_allow\.conf";', "include `"$ClientAllowForwardPath`";"
$BackendProxy = "http://127.0.0.1:$ServerPort" $BackendProxy = "http://127.0.0.1:$ServerPort"
$NewConfContent = $NewConfContent -replace 'proxy_pass\s+http://(127\.0\.0\.1|localhost):\d+;', "proxy_pass $BackendProxy;" $NewConfContent = [regex]::Replace(
$NewConfContent,
'(location\s+/api/\s*\{[\s\S]*?proxy_pass\s+)http://(127\.0\.0\.1|localhost):\d+(;)',
"`${1}$BackendProxy`${3}"
)
$NewConfContent = [regex]::Replace(
$NewConfContent,
'(location\s+/api/tasks/active/stream\s*\{[\s\S]*?proxy_pass\s+)http://(127\.0\.0\.1|localhost):\d+(;)',
"`${1}$BackendProxy`${3}"
)
# 使用 UTF8 无 BOM 编码写入 # 使用 UTF8 无 BOM 编码写入
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false $Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText("$NginxConfPath", $NewConfContent, $Utf8NoBom) [System.IO.File]::WriteAllText("$NginxConfPath", $NewConfContent, $Utf8NoBom)
@@ -533,6 +726,9 @@ $BackendProc = Start-PythonBackground -ScriptPath "run_backend.py"
if (-not (Assert-ProcessAlive -Process $BackendProc -DisplayName "Backend")) { if (-not (Assert-ProcessAlive -Process $BackendProc -DisplayName "Backend")) {
return return
} }
if (-not (Wait-BackendReady -Port $ServerPort -TimeoutSeconds $BackendReadyTimeoutSeconds)) {
return
}
# 6.5 Start job worker # 6.5 Start job worker
Write-Host ">>> Launching job worker..." -ForegroundColor Green Write-Host ">>> Launching job worker..." -ForegroundColor Green
@@ -541,6 +737,12 @@ if (-not (Assert-ProcessAlive -Process $WorkerProc -DisplayName "Worker")) {
return return
} }
# 6.6 Start tile-server
Start-TileServer
if ($global:LASTEXITCODE -eq 1) {
return
}
# 7. Start Nginx # 7. Start Nginx
if (Test-Path -LiteralPath "$NginxExe") { if (Test-Path -LiteralPath "$NginxExe") {
Write-Host ">>> Launching Nginx..." -ForegroundColor Green Write-Host ">>> Launching Nginx..." -ForegroundColor Green
@@ -575,6 +777,9 @@ if (Test-Path -LiteralPath "$NginxExe") {
Write-StatusLine "Frontend (via Nginx): http://$DisplayHost" Write-StatusLine "Frontend (via Nginx): http://$DisplayHost"
Write-StatusLine "Backend (internal): http://127.0.0.1`:$ServerPort" Write-StatusLine "Backend (internal): http://127.0.0.1`:$ServerPort"
Write-StatusLine "API Docs (internal): http://127.0.0.1`:$ServerPort/docs" Write-StatusLine "API Docs (internal): http://127.0.0.1`:$ServerPort/docs"
if ($TileServerAutoStart -and $TileServerUrl) {
Write-StatusLine "Tile Server: $TileServerUrl"
}
Write-StatusLine "============================================================" Green Write-StatusLine "============================================================" Green
Write-Host "" Write-Host ""
Write-StatusLine "System is running. Press Ctrl+C to stop all services." Yellow Write-StatusLine "System is running. Press Ctrl+C to stop all services." Yellow
@@ -599,5 +804,6 @@ try {
Stop-Backend-By-Cmdline -MatchText "run_backend.py" Stop-Backend-By-Cmdline -MatchText "run_backend.py"
Stop-Backend-By-Cmdline -MatchText "run_worker.py" Stop-Backend-By-Cmdline -MatchText "run_worker.py"
Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe
Stop-TileServer
Write-Host "Done." -ForegroundColor Green Write-Host "Done." -ForegroundColor Green
} }
+94
View File
@@ -0,0 +1,94 @@
# InSAR Management System - Stop Script (PowerShell)
$ErrorActionPreference = "Continue"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$ProjectRoot = Split-Path -Parent $ScriptDir
Set-Location -LiteralPath "$ProjectRoot"
$EnvPath = Join-Path $ProjectRoot ".env"
$NginxExe = "C:/nginx-1.29.4/nginx.exe"
$TileServerAutoStop = $true
$TileServerRoot = ""
$TileServerStopScript = "stop-all.bat"
if (Test-Path -LiteralPath "$EnvPath") {
foreach ($line in (Get-Content -LiteralPath "$EnvPath")) {
$trimmed = $line.Trim()
if ($trimmed.StartsWith("#") -or -not $trimmed.Contains("=")) { continue }
$parts = $trimmed.Split("=", 2)
$key = $parts[0].Trim()
$val = $parts[1].Trim().Trim('"').Trim("'")
if ($key -eq "NGINX_PATH" -and $val) { $NginxExe = $val }
if ($key -eq "TILE_SERVER_AUTO_STOP") { $TileServerAutoStop = -not ($val -match '^(?i)(false|0|no|off)$') }
if ($key -eq "TILE_SERVER_ROOT" -and $val) { $TileServerRoot = $val }
if ($key -eq "TILE_SERVER_STOP_SCRIPT" -and $val) { $TileServerStopScript = $val }
}
}
function Stop-Backend-By-Cmdline {
param([string]$MatchText)
$candidates = Get-CimInstance Win32_Process -Filter "Name='python.exe'" -ErrorAction SilentlyContinue
foreach ($proc in $candidates) {
if ($proc.CommandLine -and $proc.CommandLine -like "*$MatchText*") {
Write-Host "Stopping python PID $($proc.ProcessId): $MatchText"
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
}
}
}
function Stop-Process-By-Name {
param([string]$Name, [string]$ExeName)
$projectMatch = Join-Path -Path $ProjectRoot -ChildPath "nginx"
$matched = @()
$candidates = Get-CimInstance Win32_Process -Filter "Name='$Name.exe'" -ErrorAction SilentlyContinue
foreach ($proc in ($candidates | Where-Object { $_ })) {
if ($proc.CommandLine -and $proc.CommandLine -like "*$projectMatch*") {
$matched += $proc
}
}
foreach ($proc in $matched) {
Write-Host "Stopping $Name PID $($proc.ProcessId)"
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
}
}
function Stop-TileServer {
if (-not $TileServerAutoStop -or -not $TileServerRoot) {
return
}
$script = if ([System.IO.Path]::IsPathRooted($TileServerStopScript)) {
$TileServerStopScript
} else {
Join-Path -Path $TileServerRoot -ChildPath $TileServerStopScript
}
if (-not (Test-Path -LiteralPath "$script")) {
Write-Warning "tile-server stop script not found: $script"
return
}
Write-Host "Stopping tile-server..."
$previousNoPause = $env:NO_PAUSE
try {
$env:NO_PAUSE = "1"
& "$script"
} finally {
if ($null -eq $previousNoPause) {
Remove-Item Env:\NO_PAUSE -ErrorAction SilentlyContinue
} else {
$env:NO_PAUSE = $previousNoPause
}
}
}
Write-Host ">>> Stopping InSAR Management System V2..." -ForegroundColor Yellow
Stop-Backend-By-Cmdline -MatchText "run_backend.py"
Stop-Backend-By-Cmdline -MatchText "run_worker.py"
$NginxProcName = Split-Path -Leaf $NginxExe
$NginxProcName = $NginxProcName -replace '\.exe$', ''
Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe
Stop-TileServer
Write-Host "Done." -ForegroundColor Green
+17
View File
@@ -0,0 +1,17 @@
@echo off
:: =====================================================================
:: InSAR Management System - Windows One-Click Stopper
:: =====================================================================
chcp 65001 >nul
setlocal
cd /d "%~dp0"
echo [*] Stopping system services (PowerShell)...
powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\stop_app.ps1"
if %errorlevel% neq 0 (
echo.
echo [!] Stop failed.
pause
)