docs: update production workflow design and runtime changes
This commit is contained in:
+46
-16
@@ -43,6 +43,10 @@ CONDA_ENV_NAME=
|
||||
|
||||
NGINX_PATH=C:\nginx\nginx.exe
|
||||
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=
|
||||
|
||||
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
|
||||
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_UNPACK_DELETE_ARCHIVE=true
|
||||
GF3_SOURCE_DIRS=D:\GF3_L1A_Image
|
||||
GF3_SARSCAPE_NATIVE_DIRS=D:\GF3_L2_ENVI_Binary_Pool
|
||||
GF3_STORAGE_DIRS=D:\GF3_L2_Image_Pool
|
||||
GF3_SARSCAPE_WRAPPER_EXE=D:\Code\Insar_management_system_v2\.codex_tmp\GF3_L1A_To_L2_pipeline\dist\windows\gf3wrapper.exe
|
||||
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
|
||||
@@ -114,8 +120,7 @@ SRTM_DEM_DIR=D:\SRTM30m
|
||||
GF3_GEO_DEM_PATH=D:\DEM\gf3_dem.jp2
|
||||
WATER_RESULTS_DIR=D:\WaterResult
|
||||
SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready
|
||||
SAR_ANALYSIS_WORK_ROOT=D:\production_results\sar_analysis_work
|
||||
SAR_ANALYSIS_PREVIEW_ROOT=D:\production_results\sar_analysis_preview
|
||||
SAR_ANALYSIS_WORK_ROOT=D:\production_runtime\sar_analysis_work
|
||||
SAR_ANALYSIS_NODATA_VALUE=-9999
|
||||
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_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_MAX_TIMEOUT_SECONDS=43200
|
||||
@@ -153,7 +158,7 @@ ENVI_STABILITY_MAX_WAIT=3600
|
||||
WSL_DISTRO=Ubuntu-24.04
|
||||
WSL_SHARED_CONDA_ENV=insar_wsl_v1
|
||||
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
|
||||
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 should point to the prepared WGS84 DEM after the one-time conversion.
|
||||
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_PER_TASK_TIMEOUT_SECONDS=43200
|
||||
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_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_TEMPLATE_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\pyint_templates
|
||||
PYINT_WORK_ROOT=D:\Code\Insar_management_system_v2\backend\runtime\pyint_work
|
||||
PYINT_TEMPLATE_ROOT=D:\production_runtime\pyint_templates
|
||||
PYINT_WORK_ROOT=D:\production_runtime\pyint_work
|
||||
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_FABDEM_ROOT=
|
||||
# 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_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_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_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_SOURCE_ROOTS=D:\LuTan1_Image_Pool
|
||||
GAMMA_SBAS_ORBIT_ROOTS=D:\orbit_pools\envi
|
||||
@@ -290,7 +296,7 @@ TIMESERIES_ENABLED=false
|
||||
TIMESERIES_WSL_DISTRO=Ubuntu-24.04
|
||||
TIMESERIES_ENV_NAME=insar_wsl_v1
|
||||
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_ORBIT_POOL_ISCE2=D:\orbit_pools\isce2
|
||||
TIMESERIES_EXPERIMENT_ROOT=
|
||||
@@ -320,6 +326,24 @@ JOB_WORKER_STALE_RUNNING_SECONDS=7200
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
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
|
||||
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
|
||||
|
||||
@@ -65,6 +65,7 @@ tmp_dir_coreg_*/
|
||||
|
||||
# Local third-party runtime bundles
|
||||
third_party/LandSAR/
|
||||
third_party/GF3_L1A_To_L2_pipeline/
|
||||
|
||||
# Large local datasets / installers
|
||||
Data/
|
||||
|
||||
+90
-22
@@ -42,7 +42,7 @@ def _infer_conda_env_name_from_python(path: str | None) -> 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:
|
||||
@@ -77,6 +77,26 @@ def _default_result_publish_root(project_root: str) -> str:
|
||||
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]:
|
||||
env_map: dict[str, str] = {}
|
||||
if not os.path.isfile(env_path):
|
||||
@@ -197,7 +217,6 @@ class Settings(BaseSettings):
|
||||
WATER_RESULTS_DIR: str = ""
|
||||
SAR_ANALYSIS_READY_ROOT: str = ""
|
||||
SAR_ANALYSIS_WORK_ROOT: str = ""
|
||||
SAR_ANALYSIS_PREVIEW_ROOT: str = ""
|
||||
SAR_ANALYSIS_NODATA_VALUE: float = -9999.0
|
||||
SAR_ANALYSIS_OUTPUT_COG: bool = True
|
||||
|
||||
@@ -206,9 +225,11 @@ class Settings(BaseSettings):
|
||||
GF3_ARCHIVE_SOURCE_DIRS: str = ""
|
||||
GF3_ARCHIVE_EXTS: str = ".zip,.tar,.tar.gz,.tgz"
|
||||
GF3_UNPACK_DELETE_ARCHIVE: bool = True
|
||||
GF3_LEGACY_GDAL_ENABLED: bool = False
|
||||
GF3_SOURCE_DIRS: str = ""
|
||||
GF3_SARSCAPE_NATIVE_DIRS: str = ""
|
||||
GF3_STORAGE_DIRS: str = ""
|
||||
GF3_SARSCAPE_RUNTIME_DIR: 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_DEM_PATH: str = ""
|
||||
@@ -340,6 +361,7 @@ class Settings(BaseSettings):
|
||||
GAMMA_SBAS_ENV_SCRIPT: str = ""
|
||||
GAMMA_SBAS_WORK_ROOT: str = ""
|
||||
GAMMA_SBAS_PRODUCT_ROOT: str = ""
|
||||
GAMMA_SBAS_TRIAL_ROOT: str = ""
|
||||
GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT: str = ""
|
||||
GAMMA_SBAS_SOURCE_ROOTS: str = ""
|
||||
GAMMA_SBAS_ORBIT_ROOTS: str = ""
|
||||
@@ -420,19 +442,13 @@ class Settings(BaseSettings):
|
||||
object.__setattr__(
|
||||
self,
|
||||
"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:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"SAR_ANALYSIS_WORK_ROOT",
|
||||
os.path.join(backend_dir, "runtime", "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"),
|
||||
_default_runtime_dir(project_root, "sar_analysis_work"),
|
||||
)
|
||||
object.__setattr__(
|
||||
self,
|
||||
@@ -441,11 +457,29 @@ class Settings(BaseSettings):
|
||||
)
|
||||
if not self.SRTM_DEM_DIR:
|
||||
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:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"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:
|
||||
object.__setattr__(
|
||||
@@ -496,7 +530,7 @@ class Settings(BaseSettings):
|
||||
object.__setattr__(
|
||||
self,
|
||||
"ISCE2_WORK_ROOT",
|
||||
os.path.join(backend_dir, "runtime", "isce2_work"),
|
||||
_default_runtime_dir(project_root, "isce2_work"),
|
||||
)
|
||||
if not self.ISCE2_OUTPUT_ROOT:
|
||||
object.__setattr__(
|
||||
@@ -582,7 +616,7 @@ class Settings(BaseSettings):
|
||||
object.__setattr__(
|
||||
self,
|
||||
"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:
|
||||
object.__setattr__(self, "ISCE2_RUNTIME_ID", "isce2_runtime_v1")
|
||||
@@ -608,13 +642,13 @@ class Settings(BaseSettings):
|
||||
object.__setattr__(
|
||||
self,
|
||||
"PYINT_TEMPLATE_ROOT",
|
||||
os.path.join(backend_dir, "runtime", "pyint_templates"),
|
||||
_default_runtime_dir(project_root, "pyint_templates"),
|
||||
)
|
||||
if not self.PYINT_WORK_ROOT:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"PYINT_WORK_ROOT",
|
||||
os.path.join(backend_dir, "runtime", "pyint_work"),
|
||||
_default_runtime_dir(project_root, "pyint_work"),
|
||||
)
|
||||
if not self.PYINT_OUTPUT_ROOT:
|
||||
object.__setattr__(
|
||||
@@ -626,7 +660,7 @@ class Settings(BaseSettings):
|
||||
object.__setattr__(
|
||||
self,
|
||||
"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"
|
||||
if pyint_dem_mode not in {"local_fabdem", "opentopo", "prepared_file"}:
|
||||
@@ -743,7 +777,7 @@ class Settings(BaseSettings):
|
||||
object.__setattr__(
|
||||
self,
|
||||
"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:
|
||||
object.__setattr__(
|
||||
@@ -751,6 +785,12 @@ class Settings(BaseSettings):
|
||||
"GAMMA_SBAS_PRODUCT_ROOT",
|
||||
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:
|
||||
object.__setattr__(
|
||||
self,
|
||||
@@ -848,7 +888,7 @@ class Settings(BaseSettings):
|
||||
object.__setattr__(
|
||||
self,
|
||||
"TIMESERIES_WORK_ROOT",
|
||||
os.path.join(backend_dir, "runtime", "timeseries_work"),
|
||||
_default_runtime_dir(project_root, "timeseries_work"),
|
||||
)
|
||||
if not self.TIMESERIES_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.SAR_ANALYSIS_READY_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.PYINT_TEMPLATE_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:
|
||||
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_TRIAL_ROOT, exist_ok=True)
|
||||
os.makedirs(settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT, exist_ok=True)
|
||||
if settings.LANDSAR_SBAS_ENABLED:
|
||||
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="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_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)
|
||||
for label, value in (
|
||||
("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),
|
||||
("MONITOR_RADAR_DIRS", settings.MONITOR_RADAR_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_STORAGE_DIRS", settings.GF3_STORAGE_DIRS),
|
||||
("GF3_SARSCAPE_RUNTIME_DIR", settings.GF3_SARSCAPE_RUNTIME_DIR),
|
||||
):
|
||||
values = split_env_paths(raw_value)
|
||||
if not values:
|
||||
@@ -1155,6 +1201,21 @@ def validate_runtime_config() -> dict[str, Any]:
|
||||
for item in values:
|
||||
_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 not settings.ISCE2_WSL_DISTRO:
|
||||
errors.append("ISCE2_ENABLED=true 但 ISCE2_WSL_DISTRO 未配置。")
|
||||
@@ -1279,6 +1340,13 @@ def validate_runtime_config() -> dict[str, Any]:
|
||||
warnings=warnings,
|
||||
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(
|
||||
label="GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT",
|
||||
value=settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT,
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ async def lifespan(app: FastAPI):
|
||||
sbas_catalog_bootstrap = await sbas_insar_catalog_service.bootstrap_catalog_on_startup_clean()
|
||||
except Exception as exc:
|
||||
sbas_catalog_bootstrap = {
|
||||
"storage_root": settings.GAMMA_SBAS_WORK_ROOT,
|
||||
"storage_root": settings.GAMMA_SBAS_PRODUCT_ROOT,
|
||||
"manifest_count": 0,
|
||||
"db_count": 0,
|
||||
"needs_rebuild": False,
|
||||
|
||||
@@ -260,4 +260,8 @@ async def get_dinsar_product_preview(
|
||||
if not preview_path or not os.path.isfile(preview_path):
|
||||
raise HTTPException(status_code=404, detail="Preview not found")
|
||||
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"},
|
||||
)
|
||||
|
||||
@@ -43,8 +43,10 @@ class MonitorConfig(BaseModel):
|
||||
dinsar_dirs: List[str] = []
|
||||
gf3_archive_source_dirs: List[str] = []
|
||||
gf3_source_dirs: List[str] = []
|
||||
gf3_legacy_gdal_enabled: bool = False
|
||||
gf3_sarscape_native_dirs: List[str] = []
|
||||
gf3_storage_dirs: List[str] = []
|
||||
gf3_sarscape_runtime_dir: Optional[str] = None
|
||||
gf3_sarscape_wrapper_exe: Optional[str] = None
|
||||
gf3_sarscape_idlrt_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,过滤已处理的,逐个处理并自动入库。
|
||||
"""
|
||||
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 []
|
||||
if not gf3_source_dirs:
|
||||
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 预处理输入。
|
||||
"""
|
||||
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_source_dirs = MONITOR_CONFIG.get("gf3_source_dirs") or []
|
||||
if not gf3_archive_source_dirs:
|
||||
|
||||
@@ -142,4 +142,8 @@ async def get_psinsar_product_preview(
|
||||
preview_path = str(detail.get("preview_path") or "").strip()
|
||||
if not preview_path or not os.path.isfile(preview_path):
|
||||
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,
|
||||
media_type=media_type,
|
||||
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,
|
||||
media_type=media_type,
|
||||
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,
|
||||
media_type=media_type,
|
||||
filename=artifact_path.name,
|
||||
headers={"Cache-Control": "public, max-age=31536000, immutable"},
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ from .dependencies import _add_operation_audit_log, _get_current_user, _require_
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
STATIC_ASSET_CACHE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
|
||||
|
||||
|
||||
class SbasInsarCatalogRebuildRequest(BaseModel):
|
||||
@@ -189,7 +190,7 @@ async def get_sbas_insar_product_preview(
|
||||
preview_path = str(detail.get("preview_path") or "").strip()
|
||||
if not preview_path or not os.path.isfile(preview_path):
|
||||
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}")
|
||||
@@ -209,4 +210,5 @@ async def get_sbas_insar_product_asset(
|
||||
asset.absolute_path,
|
||||
media_type=asset.media_type or "application/octet-stream",
|
||||
filename=asset.asset_name or os.path.basename(asset.absolute_path),
|
||||
headers=STATIC_ASSET_CACHE_HEADERS,
|
||||
)
|
||||
|
||||
@@ -902,6 +902,14 @@ async def submit_gf3_process(
|
||||
admin_user: AuthUserORM = Depends(_require_admin),
|
||||
):
|
||||
"""提交 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):
|
||||
raise HTTPException(status_code=400, detail=f"输入路径不存在: {req.input_dir}")
|
||||
|
||||
|
||||
@@ -21,8 +21,10 @@ MONITOR_CONFIG = {
|
||||
# GF3 链路
|
||||
"gf3_archive_source_dirs": split_env_paths(settings.GF3_ARCHIVE_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_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_idlrt_path": settings.GF3_SARSCAPE_IDLRT_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:
|
||||
candidate = (
|
||||
Path(settings.PROJECT_ROOT)
|
||||
/ ".codex_tmp"
|
||||
/ "third_party"
|
||||
/ "GF3_L1A_To_L2_pipeline"
|
||||
/ "dist"
|
||||
/ "windows"
|
||||
@@ -443,7 +443,12 @@ def run_gf3_sarscape_production(
|
||||
"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)
|
||||
config_path = runtime_dir / "gf3wrapper.json"
|
||||
env = os.environ.copy()
|
||||
|
||||
@@ -969,7 +969,11 @@ async def _check_source_roots() -> Dict[str, Any]:
|
||||
|
||||
for path in split_env_paths(settings.GF3_SOURCE_DIRS):
|
||||
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)
|
||||
|
||||
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"
|
||||
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()
|
||||
if wrapper_exe:
|
||||
status = _probe_file_status(wrapper_exe)
|
||||
@@ -1017,7 +1027,6 @@ async def _check_sar_analysis_ready() -> Dict[str, Any]:
|
||||
roots = {
|
||||
"ready": _probe_directory_status(settings.SAR_ANALYSIS_READY_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():
|
||||
payload["role"] = role
|
||||
|
||||
@@ -3612,6 +3612,12 @@ async def _handle_gf3_process(job: SystemJobORM) -> None:
|
||||
"""GF3 L1A→L2 处理 job handler(辐射定标 + RPC 几何校正)。"""
|
||||
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 {}
|
||||
processing_id = payload.get("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."""
|
||||
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:
|
||||
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 .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 {}
|
||||
source_dirs = payload.get("source_dirs") or []
|
||||
if not source_dirs:
|
||||
|
||||
@@ -271,6 +271,16 @@ def _build_root_specs_from_settings() -> List[RootSpec]:
|
||||
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(
|
||||
_iter_single_root_specs(
|
||||
env_var="SAR_ANALYSIS_READY_ROOT",
|
||||
@@ -438,6 +448,46 @@ def _build_root_specs_from_settings() -> List[RootSpec]:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
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:
|
||||
radius_m = 6371008.8
|
||||
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]:
|
||||
source_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate.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"
|
||||
if not lt_path.is_file():
|
||||
candidates = sorted((run_dir / "dem").glob("*.lt_fine"))
|
||||
lt_path = candidates[0] if candidates else lt_path
|
||||
mli_par = run_dir / "sbas" / "mli.ave.par"
|
||||
disp_tab = run_dir / "sbas" / "disp.TS_tab"
|
||||
work_run_dir = _expert_gamma_work_run_dir(run_dir)
|
||||
work_manifest = _safe_read_json(work_run_dir / "run_manifest.json")
|
||||
if not work_manifest:
|
||||
work_manifest = _safe_read_json(run_dir / "run_manifest.json")
|
||||
lt_path = _expert_gamma_lookup_path(work_run_dir, work_manifest)
|
||||
mli_par = work_run_dir / "sbas" / "mli.ave.par"
|
||||
disp_tab = work_run_dir / "sbas" / "disp.TS_tab"
|
||||
if not source_path.is_file():
|
||||
raise FileNotFoundError("geo_los_def_rate.tif is missing")
|
||||
if not lt_path.is_file():
|
||||
@@ -852,10 +897,12 @@ def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: floa
|
||||
img_y = min(max(0, img_y), radar_lines - 1)
|
||||
|
||||
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()
|
||||
if line.strip()
|
||||
]
|
||||
dates = _read_expert_sbas_dates(work_run_dir)
|
||||
if not dates:
|
||||
dates = _read_expert_sbas_dates(run_dir)
|
||||
displacements: list[dict[str, Any]] = []
|
||||
for index, disp_path in enumerate(raw_disp_paths):
|
||||
@@ -872,6 +919,8 @@ def _query_expert_gamma_point_timeseries(run_dir: Path, *, lon: float, lat: floa
|
||||
return {
|
||||
"schema": "insar.gamma-sbas-point-query/v1",
|
||||
"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},
|
||||
"matched": {
|
||||
**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"
|
||||
coverage_path = run_dir / "publish" / "geotiff" / "geo_los_def_rate_rgb.tif"
|
||||
work_run_dir = _expert_gamma_work_run_dir(run_dir)
|
||||
manifest = _safe_read_json(work_run_dir / "run_manifest.json")
|
||||
if not manifest:
|
||||
manifest = _safe_read_json(run_dir / "run_manifest.json")
|
||||
lt_path = run_dir / "dem" / f"{manifest.get('reference_date') or ''}.lt_fine"
|
||||
if not lt_path.is_file():
|
||||
candidates = sorted((run_dir / "dem").glob("*.lt_fine"))
|
||||
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():
|
||||
return {}
|
||||
|
||||
@@ -1982,6 +2031,12 @@ def _stack_dates_from_manifest(stack_manifest: dict[str, Any], manifest: dict[st
|
||||
|
||||
class SbasInsarCatalogService:
|
||||
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")
|
||||
run_root = root / "runs"
|
||||
run_root.mkdir(parents=True, exist_ok=True)
|
||||
@@ -1989,6 +2044,9 @@ class SbasInsarCatalogService:
|
||||
|
||||
def get_run_roots(self) -> list[str]:
|
||||
roots = [self.get_run_root()]
|
||||
work_root = self.get_work_run_root()
|
||||
if work_root not in roots:
|
||||
roots.append(work_root)
|
||||
try:
|
||||
landsar_root = landsar_sbas_service.configured_run_root()
|
||||
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]:
|
||||
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:
|
||||
root = Path(raw_root)
|
||||
if not root.is_dir():
|
||||
continue
|
||||
manifest_paths.extend(
|
||||
_normalize_path(path)
|
||||
for path in sorted(root.glob("*/run_manifest.json"))
|
||||
if self._is_publish_ready(path.parent, _safe_read_json(path))
|
||||
)
|
||||
return sorted(dict.fromkeys(manifest_paths))
|
||||
for path in sorted(root.glob("*/run_manifest.json")):
|
||||
if not self._is_publish_ready(path.parent, _safe_read_json(path)):
|
||||
continue
|
||||
run_id = path.parent.name
|
||||
manifest_paths_by_run.setdefault(run_id, _normalize_path(path))
|
||||
return list(manifest_paths_by_run.values())
|
||||
|
||||
@staticmethod
|
||||
def _is_landsar_manifest(manifest: dict[str, Any]) -> bool:
|
||||
@@ -2295,9 +2353,12 @@ class SbasInsarCatalogService:
|
||||
if self._is_landsar_manifest(manifest):
|
||||
return self._build_landsar_product(run_dir, manifest_file, manifest)
|
||||
|
||||
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 {}
|
||||
stack_manifest = _safe_read_json(run_dir / "stack_manifest.json")
|
||||
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")
|
||||
workflow_summary = _safe_read_json(run_dir / "workflow_summary.json")
|
||||
is_expert_gamma = self._is_expert_gamma_manifest(manifest, run_dir)
|
||||
|
||||
@@ -786,8 +786,20 @@ class SbasInsarProductionService:
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.trial_root = Path(settings.BACKEND_DIR) / "runtime" / "gamma_ipta_trials"
|
||||
self.production_root = Path(settings.GAMMA_SBAS_WORK_ROOT or (Path(settings.BACKEND_DIR) / "runtime" / "sbas_insar_production"))
|
||||
project_drive = Path(settings.PROJECT_ROOT).drive
|
||||
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]:
|
||||
return {
|
||||
@@ -797,6 +809,7 @@ class SbasInsarProductionService:
|
||||
"implementation_state": "expert_manifest_script_runner_primary",
|
||||
"trial_root": str(self.trial_root),
|
||||
"production_root": str(self.production_root),
|
||||
"product_root": str(self.product_root),
|
||||
"min_common_overlap_ratio": self._effective_min_common_overlap_ratio(None),
|
||||
"workflow_runner": {
|
||||
"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(manifest_path, 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)
|
||||
|
||||
def list_trial_runs(self) -> dict[str, Any]:
|
||||
@@ -3828,6 +3843,8 @@ class SbasInsarProductionService:
|
||||
run_manifest["next_stage"] = "fix_workflow"
|
||||
self._write_json(manifest_path, run_manifest)
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
@@ -5204,6 +5221,7 @@ class SbasInsarProductionService:
|
||||
errors.append(f"{label} does not point to an existing Gamma DEM + .par pair: {raw_path}")
|
||||
|
||||
cache_roots = [
|
||||
Path(settings.PYINT_DEM_ROOT),
|
||||
Path(settings.BACKEND_DIR) / "runtime" / "pyint_dem",
|
||||
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")
|
||||
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
|
||||
def _write_json(path: Path, payload: dict[str, Any]) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -24,6 +24,14 @@ def _project_file_windows(*relative_parts: str) -> str:
|
||||
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)
|
||||
class WslRuntimeDefinition:
|
||||
runtime_id: str
|
||||
@@ -106,7 +114,7 @@ def build_wsl_runtime_registry() -> WslRuntimeRegistry:
|
||||
).strip()
|
||||
broker_job_root_windows = os.path.normpath(
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
@@ -111,11 +111,37 @@ ORBIT_POOL_LANDSAR=
|
||||
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_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
|
||||
```
|
||||
|
||||
### 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 环境”模式。
|
||||
|
||||
@@ -123,7 +149,7 @@ ENVI_TASK_TIMEOUT_SECONDS=21600
|
||||
WSL_DISTRO=Ubuntu-24.04
|
||||
WSL_SHARED_CONDA_ENV=insar_wsl_v1
|
||||
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
|
||||
PYINT_RUNTIME_ID=gamma_pyint_runtime_v1
|
||||
@@ -138,7 +164,7 @@ PYINT_RUNTIME_ID=gamma_pyint_runtime_v1
|
||||
使用共享 python,runner 为 `deploy/wsl/runners/gamma_pyint_runner.py`
|
||||
Gamma 固定环境脚本为 `deploy/wsl/profiles/gamma_env.sh`
|
||||
|
||||
### 3.6 ISCE2 D-InSAR 与旧时序兼容
|
||||
### 3.7 ISCE2 D-InSAR 与旧时序兼容
|
||||
|
||||
```env
|
||||
ISCE2_ENABLED=false
|
||||
@@ -156,7 +182,7 @@ TIMESERIES_PYTHON=/home/administrator/miniconda3/envs/insar_wsl_v1/bin/python
|
||||
- 旧 ISCE2/MintPy 时序生产链默认关闭,不再作为 SBAS 生产入口。
|
||||
- 如果必须做历史链路对比,需要显式开启 `TIMESERIES_ENABLED=true` 并提供完整旧脚本路径。
|
||||
|
||||
### 3.7 Gamma / PyINT / SBAS
|
||||
### 3.8 Gamma / PyINT / SBAS
|
||||
|
||||
```env
|
||||
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/tif;Sentinel-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 / PyINT;LT-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:
|
||||
|
||||
- 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.
|
||||
- `/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`.
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
| `pixel_size_m` | 近似像元大小 |
|
||||
| `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
|
||||
|
||||
@@ -508,4 +508,3 @@ options: dict | None = None
|
||||
- 支持模型版本;
|
||||
- 输出概率图和二值图;
|
||||
- 与 RF/baseline 可切换。
|
||||
|
||||
|
||||
@@ -19,12 +19,15 @@ GF3 原始压缩包池
|
||||
|
||||
## 2. 目录约定
|
||||
|
||||
推荐继续沿用现场已有目录语义,并新增一个 ENVI/SARscape 原生池。
|
||||
推荐把输入、生产结果和运行时目录分开,避免把系统工作文件混入业务结果池。
|
||||
|
||||
```env
|
||||
GF3_ARCHIVE_SOURCE_DIRS=D:\GF3_Image_Pool_Zip
|
||||
GF3_SARSCAPE_NATIVE_DIRS=D:\GF3_L2_ENVI_Binary_Pool
|
||||
GF3_STORAGE_DIRS=D:\GF3_L2_Image_Pool
|
||||
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
|
||||
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_SOURCE_DIRS` | legacy Python/GDAL L1A 解包输入,默认关闭 | 否 |
|
||||
| `GF3_SARSCAPE_NATIVE_DIRS` | SARscape `_geo` 原生结果池 | 否 |
|
||||
| `GF3_STORAGE_DIRS` | GF3 标准 GeoTIFF 池 | 是 |
|
||||
| `GF3_SARSCAPE_RUNTIME_DIR` | wrapper 配置、IDL 运行时临时文件 | 否 |
|
||||
| `SAR_ANALYSIS_READY_ROOT` | 洪涝/水体分析级统一输入 | 是 |
|
||||
|
||||
生产服务器可以不部署完整管理系统。只要把完成后的 `_geo` 原生结果组放入 `GF3_SARSCAPE_NATIVE_DIRS`,管理系统就可以扫描、转换和入库。
|
||||
@@ -44,7 +49,7 @@ SAR_ANALYSIS_READY_ROOT=D:\production_results\sar_analysis_ready
|
||||
原生池以批次日期或人工批次号分组。单景目录名尽量保持 GF3 原始产品名。
|
||||
|
||||
```text
|
||||
D:\GF3_L2_ENVI_Binary_Pool
|
||||
D:\production_results\gf3\sarscape_native
|
||||
20260514
|
||||
GF3_MH1_FSII_051377_E132.3_N48.2_20260514_L1A_HHHV_L10007356478
|
||||
GF3_MH1_FSII_..._hh_geo
|
||||
@@ -110,7 +115,7 @@ SLC 中间产物
|
||||
系统从原生池转换后写入 `GF3_STORAGE_DIRS`。
|
||||
|
||||
```text
|
||||
D:\GF3_L2_Image_Pool
|
||||
D:\production_results\gf3\standard_l2
|
||||
20260514
|
||||
GF3_MH1_FSII_051377_E132.3_N48.2_20260514_L1A_HHHV_L10007356478
|
||||
HH_L2.tif
|
||||
@@ -269,8 +274,8 @@ status=DONE
|
||||
{
|
||||
"schema": "gf3_sarscape_native.v1",
|
||||
"scene_name": "GF3_MH1_FSII_...",
|
||||
"native_dir": "D:\\GF3_L2_ENVI_Binary_Pool\\20260514\\GF3_MH1_FSII_...",
|
||||
"source_archive": "D:\\GF3_Image_Pool_Zip\\20260514\\GF3_MH1_FSII_....tar.gz",
|
||||
"native_dir": "D:\\production_results\\gf3\\sarscape_native\\20260514\\GF3_MH1_FSII_...",
|
||||
"source_archive": "D:\\production_inputs\\gf3\\archives\\20260514\\GF3_MH1_FSII_....tar.gz",
|
||||
"polarizations": ["HH", "HV"],
|
||||
"status": "NATIVE_READY",
|
||||
"assets": [
|
||||
@@ -295,8 +300,8 @@ status=DONE
|
||||
{
|
||||
"schema": "gf3_standard_geotiff.v1",
|
||||
"scene_name": "GF3_MH1_FSII_...",
|
||||
"native_manifest": "D:\\GF3_L2_ENVI_Binary_Pool\\...\\gf3_native_manifest.json",
|
||||
"standard_dir": "D:\\GF3_L2_Image_Pool\\20260514\\GF3_MH1_FSII_...",
|
||||
"native_manifest": "D:\\production_results\\gf3\\sarscape_native\\...\\gf3_native_manifest.json",
|
||||
"standard_dir": "D:\\production_results\\gf3\\standard_l2\\20260514\\GF3_MH1_FSII_...",
|
||||
"status": "DONE",
|
||||
"converter": {
|
||||
"name": "gf3_sarscape_geo_to_tif",
|
||||
@@ -446,7 +451,7 @@ GF3_ARCHIVE_SOURCE_DIRS
|
||||
新增配置:
|
||||
|
||||
```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_DEM_PATH=D:\DEM\GMTED2010.jp2
|
||||
GF3_SARSCAPE_POLARIZATIONS=HH,HV
|
||||
@@ -473,4 +478,4 @@ GF3_SARSCAPE_PRODUCE_TIMEOUT_SECONDS=0
|
||||
- 每景写 `gf3_cleanup_manifest.json`,记录删除条目和释放字节数。
|
||||
- 不删除 `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
@@ -1,6 +1,6 @@
|
||||
# 文档索引
|
||||
|
||||
最后更新:2026-05-30
|
||||
最后更新:2026-06-14
|
||||
|
||||
本页是当前有效文档入口。没有列在本页的历史设计、实验记录和过程文档不再作为当前系统事实依据。
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
- [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
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)
|
||||
文档治理规则、事实来源优先级和清理约定。
|
||||
|
||||
@@ -24,10 +26,13 @@
|
||||
## 生产与结果
|
||||
|
||||
- [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)
|
||||
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)
|
||||
当前 Gamma SBAS-InSAR 生产、AOI 选栈、结果 catalog、产物和 LOS 符号约定。
|
||||
@@ -41,10 +46,10 @@
|
||||
PROJ / GDAL 配置说明。
|
||||
|
||||
- [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 生产链路稳定性约束。
|
||||
ISCE2 生产链路历史稳定性约束。新 D-InSAR 生产不再采用。
|
||||
|
||||
## 数据与业务模块
|
||||
|
||||
|
||||
Binary file not shown.
@@ -2,6 +2,8 @@
|
||||
|
||||
更新日期: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. 目标
|
||||
|
||||
本设计解决三个长期问题:
|
||||
|
||||
@@ -66,12 +66,13 @@ GAMMA_SBAS_DEFAULT_MB_MODE
|
||||
GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW
|
||||
GAMMA_SBAS_STEP_TIMEOUT_SECONDS
|
||||
GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS
|
||||
GAMMA_SBAS_TRIAL_ROOT
|
||||
```
|
||||
|
||||
默认工作根:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
`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. 生产流程
|
||||
|
||||
用户侧推荐顺序:
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:8900 http://127.0.0.1:8910 https://*.tile.openstreetmap.org https://*.tile.opentopomap.org; connect-src 'self' http://127.0.0.1:8900 http://127.0.0.1:8910; font-src 'self' data:; worker-src blob:;" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.tile.openstreetmap.org https://*.tile.opentopomap.org; connect-src 'self'; font-src 'self' data:; worker-src blob:;" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
|
||||
@@ -9,8 +9,10 @@ const DEFAULT_MONITOR_CONFIG = {
|
||||
dinsar_dirs: [],
|
||||
gf3_archive_source_dirs: [],
|
||||
gf3_source_dirs: [],
|
||||
gf3_legacy_gdal_enabled: false,
|
||||
gf3_sarscape_native_dirs: [],
|
||||
gf3_storage_dirs: [],
|
||||
gf3_sarscape_runtime_dir: '',
|
||||
gf3_sarscape_wrapper_exe: '',
|
||||
gf3_sarscape_idlrt_path: '',
|
||||
gf3_sarscape_dem_path: '',
|
||||
@@ -302,6 +304,7 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
const hasGf3SourceDirs = config.gf3_source_dirs.length > 0;
|
||||
const hasGf3SarscapeNativeDirs = config.gf3_sarscape_native_dirs.length > 0;
|
||||
const hasGf3StorageDirs = config.gf3_storage_dirs.length > 0;
|
||||
const gf3LegacyGdalEnabled = Boolean(config.gf3_legacy_gdal_enabled);
|
||||
const hasGf3SarscapeWrapper = typeof config.gf3_sarscape_wrapper_exe === 'string' && config.gf3_sarscape_wrapper_exe.trim() !== '';
|
||||
const hasGf3SarscapeDem = typeof config.gf3_sarscape_dem_path === 'string' && config.gf3_sarscape_dem_path.trim() !== '';
|
||||
|
||||
@@ -311,8 +314,8 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
const canRunS1Scan = !readOnly && configLoaded && hasS1SourceDirs;
|
||||
const canRunS1OrbitScan = !readOnly && configLoaded && hasS1OrbitDirs;
|
||||
const canRunGf3Scan = !readOnly && configLoaded && hasGf3StorageDirs;
|
||||
const canRunGf3Unpack = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SourceDirs;
|
||||
const canRunGf3Process = !readOnly && configLoaded && hasGf3SourceDirs;
|
||||
const canRunGf3Unpack = !readOnly && configLoaded && gf3LegacyGdalEnabled && hasGf3ArchiveSourceDirs && hasGf3SourceDirs;
|
||||
const canRunGf3Process = !readOnly && configLoaded && gf3LegacyGdalEnabled && hasGf3SourceDirs;
|
||||
const canRunGf3SarscapeProduce = !readOnly && configLoaded && hasGf3ArchiveSourceDirs && hasGf3SarscapeNativeDirs && hasGf3StorageDirs && hasGf3SarscapeWrapper && hasGf3SarscapeDem;
|
||||
const canRunGf3SarscapeSync = !readOnly && configLoaded && hasGf3SarscapeNativeDirs && hasGf3StorageDirs;
|
||||
const canRunGf3SarscapeClean = !readOnly && configLoaded && hasGf3SarscapeNativeDirs && hasGf3StorageDirs;
|
||||
@@ -797,9 +800,10 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
<div style={rowStyle}><span style={labelStyle}>S1 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_storage_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>S1 精轨</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.s1_orbit_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>GF3 压缩包</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_archive_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>GF3 来源</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>GF3 legacy L1A</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>GF3 原生</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_sarscape_native_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>GF3 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_storage_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>GF3 runtime</span><span style={{ wordBreak: 'break-all' }}>{config.gf3_sarscape_runtime_dir || '未配置'}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>D-InSAR 结果</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.dinsar_dirs)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -888,31 +892,36 @@ const DataMonitorPanel = ({ apiEndpoint, onTaskStart, readOnly = false, enabled
|
||||
</div>
|
||||
|
||||
<div style={sectionStyle}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>GF3 归档预处理</div>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--color-text-primary)' }}>GF3 SARscape 生产</div>
|
||||
<div style={{ ...gridStyle, marginBottom: '8px' }}>
|
||||
<div style={rowStyle}><span style={labelStyle}>压缩包来源</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_archive_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>L1A 来源</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_source_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>SARscape 原生</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_sarscape_native_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>L2 存储</span><span style={{ wordBreak: 'break-all' }}>{formatList(config.gf3_storage_dirs)}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>Runtime</span><span style={{ wordBreak: 'break-all' }}>{config.gf3_sarscape_runtime_dir || '未配置'}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>Wrapper</span><span style={{ wordBreak: 'break-all' }}>{config.gf3_sarscape_wrapper_exe || '未配置'}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>SARscape DEM</span><span style={{ wordBreak: 'break-all' }}>{config.gf3_sarscape_dem_path || '未配置'}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>极化</span><span>{config.gf3_sarscape_polarizations || 'HH,HV'}</span></div>
|
||||
<div style={rowStyle}><span style={labelStyle}>Legacy GDAL</span><span>{gf3LegacyGdalEnabled ? '启用' : '关闭'}</span></div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
{gf3LegacyGdalEnabled && (
|
||||
<>
|
||||
<button
|
||||
onClick={handleGf3Unpack}
|
||||
disabled={gf3UnpackLoading || readOnly || !canRunGf3Unpack}
|
||||
style={actionBtnStyle(gf3UnpackLoading, readOnly || !canRunGf3Unpack)}
|
||||
>
|
||||
{gf3UnpackLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 解包')}
|
||||
{gf3UnpackLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 legacy 解包')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGf3BatchProcess}
|
||||
disabled={gf3ProcessLoading || readOnly || !canRunGf3Process}
|
||||
style={actionBtnStyle(gf3ProcessLoading, readOnly || !canRunGf3Process)}
|
||||
>
|
||||
{gf3ProcessLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 预处理')}
|
||||
{gf3ProcessLoading ? '运行中...' : (readOnly ? '只读模式' : 'GF3 legacy 预处理')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={handleGf3SarscapeProduce}
|
||||
disabled={gf3SarscapeProduceLoading || readOnly || !canRunGf3SarscapeProduce}
|
||||
|
||||
@@ -38,8 +38,11 @@ export const TILE_LAYER_OPTIONS = {
|
||||
maxZoom: 16,
|
||||
maxNativeZoom: 16,
|
||||
tms: false,
|
||||
updateWhenIdle: true,
|
||||
keepBuffer: 4,
|
||||
updateWhenIdle: false,
|
||||
updateWhenZooming: false,
|
||||
keepBuffer: 2,
|
||||
reuseTiles: true,
|
||||
crossOrigin: 'anonymous',
|
||||
};
|
||||
|
||||
export const TILE_LAYER_BOUNDS = [
|
||||
|
||||
@@ -10,8 +10,8 @@ const TASK_UI_POLICIES = {
|
||||
SCAN_HAZARD: { label: '灾害点同步', featureScope: 'hazard' },
|
||||
UNPACK_ARCHIVES: { label: 'LT-1 解包', featureScope: 'data_monitor' },
|
||||
UNPACK_SENTINEL1: { label: 'Sentinel-1 解包', featureScope: 'data_monitor' },
|
||||
GF3_UNPACK: { label: 'GF3 解包', featureScope: 'data_monitor' },
|
||||
GF3_BATCH_PROCESS: { label: 'GF3 预处理', featureScope: 'data_monitor' },
|
||||
GF3_UNPACK: { label: 'GF3 legacy 解包', featureScope: 'data_monitor' },
|
||||
GF3_BATCH_PROCESS: { label: 'GF3 legacy 预处理', featureScope: 'data_monitor' },
|
||||
GF3_SARSCAPE_PRODUCE: { label: 'GF3 SARscape 生产', featureScope: 'data_monitor' },
|
||||
GF3_SARSCAPE_SYNC: { label: 'GF3 SARscape 入库', featureScope: 'data_monitor' },
|
||||
GF3_SARSCAPE_CLEAN: { label: 'GF3 中间清理', featureScope: 'data_monitor' },
|
||||
@@ -73,4 +73,3 @@ export function isTaskGloballyBlocking(taskType) {
|
||||
export function getTaskTypeLabel(taskType) {
|
||||
return getTaskUiPolicy(taskType).label;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,39 @@ import apiClient from '../api/client';
|
||||
import { getHealth } from '../api/health';
|
||||
|
||||
const HEALTH_POLL_INTERVAL_MS = 30000;
|
||||
const STARTUP_RETRY_DELAYS_MS = [750, 1500, 3000];
|
||||
|
||||
const sleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
|
||||
const getErrorStatus = (error) => Number(error?.response?.status || 0);
|
||||
|
||||
const isTransientApiError = (error) => {
|
||||
const status = getErrorStatus(error);
|
||||
return !status || status === 408 || status === 429 || status >= 500;
|
||||
};
|
||||
|
||||
const requestWithTransientRetry = async (
|
||||
requestFn,
|
||||
{
|
||||
delays = STARTUP_RETRY_DELAYS_MS,
|
||||
shouldRetry = isTransientApiError,
|
||||
} = {},
|
||||
) => {
|
||||
let lastError = null;
|
||||
const retryDelays = Array.isArray(delays) ? delays : [];
|
||||
for (let attempt = 0; attempt <= retryDelays.length; attempt += 1) {
|
||||
try {
|
||||
return await requestFn();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt >= retryDelays.length || !shouldRetry(error)) {
|
||||
throw error;
|
||||
}
|
||||
await sleep(retryDelays[attempt]);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
};
|
||||
|
||||
export default function useAppAuthLifecycle({
|
||||
ensureCanOperate,
|
||||
@@ -33,15 +66,21 @@ export default function useAppAuthLifecycle({
|
||||
const { clearOnFailure = true } = options;
|
||||
const requestGeneration = authGenerationRef.current;
|
||||
try {
|
||||
const response = await apiClient.get('/auth/me', {
|
||||
const response = await requestWithTransientRetry(() => (
|
||||
apiClient.get('/auth/me', {
|
||||
skipAuthReset: !clearOnFailure,
|
||||
authGeneration: requestGeneration,
|
||||
});
|
||||
})
|
||||
));
|
||||
if (requestGeneration === authGenerationRef.current) {
|
||||
setCurrentUser(response.data || null);
|
||||
}
|
||||
} catch {
|
||||
if (clearOnFailure && requestGeneration === authGenerationRef.current) {
|
||||
} catch (error) {
|
||||
if (
|
||||
clearOnFailure
|
||||
&& getErrorStatus(error) === 401
|
||||
&& requestGeneration === authGenerationRef.current
|
||||
) {
|
||||
setCurrentUser(null);
|
||||
}
|
||||
} finally {
|
||||
@@ -88,7 +127,7 @@ export default function useAppAuthLifecycle({
|
||||
const fetchLicenseStatus = useCallback(async () => {
|
||||
try {
|
||||
setLicenseLoading(true);
|
||||
const response = await apiClient.get('/license/status');
|
||||
const response = await requestWithTransientRetry(() => apiClient.get('/license/status'));
|
||||
const data = response.data || {};
|
||||
if (!data.ok && !data.reason) {
|
||||
data.reason = '未授权';
|
||||
@@ -111,7 +150,10 @@ export default function useAppAuthLifecycle({
|
||||
setHealthLoading(true);
|
||||
}
|
||||
setHealthError('');
|
||||
const data = await getHealth(refresh ? { refresh: true } : {});
|
||||
const data = await requestWithTransientRetry(
|
||||
() => getHealth(refresh ? { refresh: true } : {}),
|
||||
{ delays: silent ? [] : STARTUP_RETRY_DELAYS_MS.slice(0, 2) },
|
||||
);
|
||||
setHealthStatus(data || null);
|
||||
} catch (error) {
|
||||
setHealthError(error.response?.data?.detail || '运维自检失败');
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
# Generated by scripts/start_app.ps1.
|
||||
# Configure NGINX_ALLOWED_CLIENT_IPS in .env. Empty means allow all clients.
|
||||
+68
-3
@@ -24,18 +24,44 @@ http {
|
||||
server {
|
||||
listen 80;
|
||||
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-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
# CSP: allow both legacy tile port 8900 and current tile-server port 8910
|
||||
# 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;
|
||||
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;
|
||||
|
||||
root "D:/Code/Insar_management_system_v2/frontend/dist";
|
||||
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 / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
@@ -49,6 +75,45 @@ http {
|
||||
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
|
||||
location /api/tasks/active/stream {
|
||||
proxy_pass http://127.0.0.1:18000;
|
||||
|
||||
@@ -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()))
|
||||
@@ -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
@@ -29,6 +29,14 @@ $CondaEnvName = ""
|
||||
$NginxExe = "C:/nginx-1.29.4/nginx.exe"
|
||||
$ServerHost = ""
|
||||
$ServerPort = 18000
|
||||
$NginxAllowedClientIps = ""
|
||||
$BackendReadyTimeoutSeconds = 120
|
||||
$TileServerAutoStart = $false
|
||||
$TileServerAutoStop = $true
|
||||
$TileServerRoot = ""
|
||||
$TileServerStartScript = "start-all.bat"
|
||||
$TileServerStopScript = "stop-all.bat"
|
||||
$TileServerUrl = ""
|
||||
|
||||
$envLines = Get-Content -LiteralPath "$EnvPath"
|
||||
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_ENV_NAME") { if ($val) { $CondaEnvName = $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_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") {
|
||||
$parsed = 0
|
||||
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_worker.py"
|
||||
$NginxProcName = Split-Path -Leaf $NginxExe
|
||||
$NginxProcName = $NginxProcName -replace '\.exe$', ''
|
||||
Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe
|
||||
if ($TileServerAutoStart -and $TileServerAutoStop) {
|
||||
Stop-TileServer
|
||||
}
|
||||
|
||||
$PortAvailable = Test-PortAvailable -Port $ServerPort
|
||||
if (-not $PortAvailable) {
|
||||
@@ -455,6 +603,38 @@ function Assert-ProcessAlive {
|
||||
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"
|
||||
if ($LastExitCode -ne 0) {
|
||||
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)
|
||||
$FrontendDist = "$ForwardRoot/frontend/dist"
|
||||
$ImageCache = "$ForwardRoot/backend/image_cache"
|
||||
$NginxClientAllowFile = Join-Path -Path $ProjectRoot -ChildPath "nginx\client_allow.conf"
|
||||
|
||||
if (-not (Test-Path -LiteralPath "$ProjectRoot/backend/image_cache")) {
|
||||
New-Item -ItemType Directory -Path "$ProjectRoot/backend/image_cache" -Force | Out-Null
|
||||
}
|
||||
Write-NginxClientAllowFile -Path "$NginxClientAllowFile"
|
||||
|
||||
$ConfContent = Get-Content -LiteralPath "$NginxConfPath" -Raw
|
||||
$NewConfContent = $ConfContent -replace 'root\s+[^;]+;', "root `"$FrontendDist`";"
|
||||
$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"
|
||||
$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 编码写入
|
||||
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||
[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")) {
|
||||
return
|
||||
}
|
||||
if (-not (Wait-BackendReady -Port $ServerPort -TimeoutSeconds $BackendReadyTimeoutSeconds)) {
|
||||
return
|
||||
}
|
||||
|
||||
# 6.5 Start job worker
|
||||
Write-Host ">>> Launching job worker..." -ForegroundColor Green
|
||||
@@ -541,6 +737,12 @@ if (-not (Assert-ProcessAlive -Process $WorkerProc -DisplayName "Worker")) {
|
||||
return
|
||||
}
|
||||
|
||||
# 6.6 Start tile-server
|
||||
Start-TileServer
|
||||
if ($global:LASTEXITCODE -eq 1) {
|
||||
return
|
||||
}
|
||||
|
||||
# 7. Start Nginx
|
||||
if (Test-Path -LiteralPath "$NginxExe") {
|
||||
Write-Host ">>> Launching Nginx..." -ForegroundColor Green
|
||||
@@ -575,6 +777,9 @@ if (Test-Path -LiteralPath "$NginxExe") {
|
||||
Write-StatusLine "Frontend (via Nginx): http://$DisplayHost"
|
||||
Write-StatusLine "Backend (internal): http://127.0.0.1`:$ServerPort"
|
||||
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-Host ""
|
||||
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_worker.py"
|
||||
Stop-Process-By-Name -Name $NginxProcName -ExeName $NginxExe
|
||||
Stop-TileServer
|
||||
Write-Host "Done." -ForegroundColor Green
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user