Improve orbit corruption diagnostics
This commit is contained in:
@@ -23,6 +23,38 @@ class StateVector:
|
|||||||
vz: float
|
vz: float
|
||||||
|
|
||||||
|
|
||||||
|
def _guess_text_encoding(input_txt: Path) -> str:
|
||||||
|
with input_txt.open("rb") as handle:
|
||||||
|
sample = handle.read(4096)
|
||||||
|
|
||||||
|
if sample.startswith(b"\xff\xfe"):
|
||||||
|
return "utf-16-le"
|
||||||
|
if sample.startswith(b"\xfe\xff"):
|
||||||
|
return "utf-16-be"
|
||||||
|
|
||||||
|
# Handle UTF-16-style wide-char text without BOM.
|
||||||
|
if sample and sample.count(b"\x00") * 4 >= len(sample):
|
||||||
|
even_nuls = sample[0::2].count(b"\x00")
|
||||||
|
odd_nuls = sample[1::2].count(b"\x00")
|
||||||
|
return "utf-16-le" if odd_nuls >= even_nuls else "utf-16-be"
|
||||||
|
|
||||||
|
return "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_line(value: str, limit: int = 200) -> str:
|
||||||
|
text = value.replace("\x00", "").replace("\t", " ").strip()
|
||||||
|
if len(text) > limit:
|
||||||
|
text = text[:limit] + "..."
|
||||||
|
return text or "<empty>"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_error(input_txt: Path, line_no: int, message: str, raw_line: str = "") -> ValueError:
|
||||||
|
detail = f"{message} (file={input_txt}, line={line_no})"
|
||||||
|
if raw_line:
|
||||||
|
detail += f". snippet={_preview_line(raw_line)!r}"
|
||||||
|
return ValueError(detail)
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Convert LT-1 GpsData .txt orbit files to ISCE2 LUTAN1 orbit XML."
|
description="Convert LT-1 GpsData .txt orbit files to ISCE2 LUTAN1 orbit XML."
|
||||||
@@ -98,40 +130,67 @@ def find_text(root: ET.Element, path: str) -> str:
|
|||||||
def parse_orbit_file(input_txt: Path) -> list[StateVector]:
|
def parse_orbit_file(input_txt: Path) -> list[StateVector]:
|
||||||
vectors: list[StateVector] = []
|
vectors: list[StateVector] = []
|
||||||
|
|
||||||
with input_txt.open("r", encoding="utf-8") as handle:
|
encoding = _guess_text_encoding(input_txt)
|
||||||
for line in handle:
|
with input_txt.open("rb") as handle:
|
||||||
|
for line_no, raw_line in enumerate(handle, start=1):
|
||||||
|
line = raw_line.decode(encoding, errors="replace").replace("\ufeff", "")
|
||||||
if not line.strip() or line.startswith("#"):
|
if not line.strip() or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if "\x00" in line:
|
||||||
|
nul_count = line.count("\x00")
|
||||||
|
raise _parse_error(
|
||||||
|
input_txt,
|
||||||
|
line_no,
|
||||||
|
(
|
||||||
|
"Orbit TXT contains NUL bytes and appears truncated or corrupted"
|
||||||
|
f" (nul_count={nul_count})"
|
||||||
|
),
|
||||||
|
raw_line=line,
|
||||||
|
)
|
||||||
|
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) < 12:
|
if len(parts) < 12:
|
||||||
continue
|
raise _parse_error(
|
||||||
|
input_txt,
|
||||||
sec_float = float(parts[5])
|
line_no,
|
||||||
sec_int = int(sec_float)
|
f"Malformed orbit record: expected at least 12 columns, got {len(parts)}",
|
||||||
microsecond = int(round((sec_float - sec_int) * 1_000_000))
|
raw_line=line,
|
||||||
|
|
||||||
timestamp = datetime(
|
|
||||||
int(parts[0]),
|
|
||||||
int(parts[1]),
|
|
||||||
int(parts[2]),
|
|
||||||
int(parts[3]),
|
|
||||||
int(parts[4]),
|
|
||||||
sec_int,
|
|
||||||
microsecond,
|
|
||||||
)
|
|
||||||
|
|
||||||
vectors.append(
|
|
||||||
StateVector(
|
|
||||||
time=timestamp,
|
|
||||||
x=float(parts[6]),
|
|
||||||
y=float(parts[7]),
|
|
||||||
z=float(parts[8]),
|
|
||||||
vx=float(parts[9]),
|
|
||||||
vy=float(parts[10]),
|
|
||||||
vz=float(parts[11]),
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
try:
|
||||||
|
sec_float = float(parts[5])
|
||||||
|
sec_int = int(sec_float)
|
||||||
|
microsecond = int(round((sec_float - sec_int) * 1_000_000))
|
||||||
|
|
||||||
|
timestamp = datetime(
|
||||||
|
int(parts[0]),
|
||||||
|
int(parts[1]),
|
||||||
|
int(parts[2]),
|
||||||
|
int(parts[3]),
|
||||||
|
int(parts[4]),
|
||||||
|
sec_int,
|
||||||
|
microsecond,
|
||||||
|
)
|
||||||
|
|
||||||
|
vectors.append(
|
||||||
|
StateVector(
|
||||||
|
time=timestamp,
|
||||||
|
x=float(parts[6]),
|
||||||
|
y=float(parts[7]),
|
||||||
|
z=float(parts[8]),
|
||||||
|
vx=float(parts[9]),
|
||||||
|
vy=float(parts[10]),
|
||||||
|
vz=float(parts[11]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise _parse_error(
|
||||||
|
input_txt,
|
||||||
|
line_no,
|
||||||
|
f"Malformed orbit record: {exc}",
|
||||||
|
raw_line=line,
|
||||||
|
) from exc
|
||||||
|
|
||||||
if not vectors:
|
if not vectors:
|
||||||
raise ValueError(f"No orbit records parsed from {input_txt}")
|
raise ValueError(f"No orbit records parsed from {input_txt}")
|
||||||
|
|||||||
@@ -161,12 +161,15 @@ async def get_orbit_status(
|
|||||||
"duplicate_count": source_stats.duplicate_count,
|
"duplicate_count": source_stats.duplicate_count,
|
||||||
"errors": source_stats.errors,
|
"errors": source_stats.errors,
|
||||||
"quarantine_path": source_gap_summary["quarantine_path"],
|
"quarantine_path": source_gap_summary["quarantine_path"],
|
||||||
|
"sample_limit": source_gap_summary["sample_limit"],
|
||||||
"source_without_isce2_count": source_gap_summary["source_without_isce2_count"],
|
"source_without_isce2_count": source_gap_summary["source_without_isce2_count"],
|
||||||
"source_without_envi_count": source_gap_summary["source_without_envi_count"],
|
"source_without_envi_count": source_gap_summary["source_without_envi_count"],
|
||||||
"envi_without_source_count": source_gap_summary["envi_without_source_count"],
|
"envi_without_source_count": source_gap_summary["envi_without_source_count"],
|
||||||
"isce2_without_source_count": source_gap_summary["isce2_without_source_count"],
|
"isce2_without_source_count": source_gap_summary["isce2_without_source_count"],
|
||||||
"suspect_bad_count": source_gap_summary["suspect_bad_count"],
|
"suspect_bad_count": source_gap_summary["suspect_bad_count"],
|
||||||
"suspect_bad_samples": source_gap_summary["suspect_bad_samples"],
|
"suspect_bad_samples": source_gap_summary["suspect_bad_samples"],
|
||||||
|
"bad_source_sample_count": source_gap_summary["bad_source_sample_count"],
|
||||||
|
"bad_source_samples": source_gap_summary["bad_source_samples"],
|
||||||
"source_without_envi_samples": source_gap_summary["source_without_envi_samples"],
|
"source_without_envi_samples": source_gap_summary["source_without_envi_samples"],
|
||||||
"envi_without_source_samples": source_gap_summary["envi_without_source_samples"],
|
"envi_without_source_samples": source_gap_summary["envi_without_source_samples"],
|
||||||
"isce2_without_source_samples": source_gap_summary["isce2_without_source_samples"],
|
"isce2_without_source_samples": source_gap_summary["isce2_without_source_samples"],
|
||||||
|
|||||||
@@ -161,25 +161,55 @@ def _inspect_orbit_txt_health(path: str) -> Dict[str, Any]:
|
|||||||
"contains_nul_bytes": False,
|
"contains_nul_bytes": False,
|
||||||
"nul_byte_count": 0,
|
"nul_byte_count": 0,
|
||||||
"tail_has_nul_bytes": False,
|
"tail_has_nul_bytes": False,
|
||||||
|
"first_nul_offset": -1,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
nul_byte_count = 0
|
nul_byte_count = 0
|
||||||
tail = b""
|
tail = b""
|
||||||
|
offset = 0
|
||||||
|
first_nul_offset = -1
|
||||||
with open(path, "rb") as stream:
|
with open(path, "rb") as stream:
|
||||||
while True:
|
while True:
|
||||||
chunk = stream.read(1024 * 1024)
|
chunk = stream.read(1024 * 1024)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
|
if first_nul_offset < 0:
|
||||||
|
chunk_nul_index = chunk.find(b"\x00")
|
||||||
|
if chunk_nul_index >= 0:
|
||||||
|
first_nul_offset = offset + chunk_nul_index
|
||||||
nul_byte_count += chunk.count(b"\x00")
|
nul_byte_count += chunk.count(b"\x00")
|
||||||
tail = (tail + chunk)[-4096:]
|
tail = (tail + chunk)[-4096:]
|
||||||
|
offset += len(chunk)
|
||||||
info["nul_byte_count"] = nul_byte_count
|
info["nul_byte_count"] = nul_byte_count
|
||||||
info["contains_nul_bytes"] = nul_byte_count > 0
|
info["contains_nul_bytes"] = nul_byte_count > 0
|
||||||
info["tail_has_nul_bytes"] = b"\x00" in tail
|
info["tail_has_nul_bytes"] = b"\x00" in tail
|
||||||
|
info["first_nul_offset"] = first_nul_offset
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
info["read_error"] = str(exc)
|
info["read_error"] = str(exc)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate_message(text: str, limit: int = 1200) -> str:
|
||||||
|
value = str(text or "").replace("\x00", "[NUL]").strip()
|
||||||
|
if len(value) <= limit:
|
||||||
|
return value
|
||||||
|
return value[:limit] + "...<truncated>"
|
||||||
|
|
||||||
|
|
||||||
|
def _compact_error_message(text: str, limit: int = 1200) -> str:
|
||||||
|
value = str(text or "").replace("\x00", "[NUL]").strip()
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
lines = [line.strip() for line in value.splitlines() if line.strip()]
|
||||||
|
if len(lines) > 1:
|
||||||
|
for line in reversed(lines):
|
||||||
|
if re.match(r"^[A-Za-z_][\w.]*:\s", line):
|
||||||
|
return _truncate_message(line, limit=limit)
|
||||||
|
|
||||||
|
return _truncate_message(value, limit=limit)
|
||||||
|
|
||||||
|
|
||||||
def _convert_to_isce2_xml_file(txt_path: str, xml_path: str) -> None:
|
def _convert_to_isce2_xml_file(txt_path: str, xml_path: str) -> None:
|
||||||
if "isce2" not in _CONVERTERS:
|
if "isce2" not in _CONVERTERS:
|
||||||
raise KeyError("未注册 isce2 轨道转换器")
|
raise KeyError("未注册 isce2 轨道转换器")
|
||||||
@@ -209,16 +239,57 @@ def _build_invalid_source_record(
|
|||||||
record: Dict[str, Any] = {
|
record: Dict[str, Any] = {
|
||||||
"name": stem,
|
"name": stem,
|
||||||
"source": txt_path,
|
"source": txt_path,
|
||||||
"error": str(error),
|
"error": _compact_error_message(error),
|
||||||
}
|
}
|
||||||
if envi_path:
|
if envi_path:
|
||||||
record["envi_path"] = envi_path
|
record["envi_path"] = envi_path
|
||||||
if isce2_path:
|
if isce2_path:
|
||||||
record["isce2_path"] = isce2_path
|
record["isce2_path"] = isce2_path
|
||||||
record.update(_inspect_orbit_txt_health(txt_path))
|
record.update(_inspect_orbit_txt_health(txt_path))
|
||||||
|
record["has_corruption_signal"] = _has_orbit_corruption_signal(record)
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def _has_orbit_corruption_signal(item: Dict[str, Any]) -> bool:
|
||||||
|
first_nul_offset = item.get("first_nul_offset", -1)
|
||||||
|
return bool(
|
||||||
|
item.get("read_error")
|
||||||
|
or item.get("contains_nul_bytes")
|
||||||
|
or item.get("tail_has_nul_bytes")
|
||||||
|
or int(item.get("nul_byte_count") or 0) > 0
|
||||||
|
or (
|
||||||
|
isinstance(first_nul_offset, (int, float))
|
||||||
|
and int(first_nul_offset) >= 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_source_gap_sample(
|
||||||
|
stem: str,
|
||||||
|
source_entry: Optional[Dict[str, Any]] = None,
|
||||||
|
envi_entry: Optional[Dict[str, Any]] = None,
|
||||||
|
isce2_entry: Optional[Dict[str, Any]] = None,
|
||||||
|
inspect_source: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
source_path = (source_entry or {}).get("path", "")
|
||||||
|
sample: Dict[str, Any] = {
|
||||||
|
"name": stem,
|
||||||
|
"source": source_path,
|
||||||
|
"source_path": source_path,
|
||||||
|
"envi_path": (envi_entry or {}).get("path", ""),
|
||||||
|
"isce2_path": (isce2_entry or {}).get("path", ""),
|
||||||
|
"has_corruption_signal": False,
|
||||||
|
}
|
||||||
|
if inspect_source and source_path:
|
||||||
|
sample.update(_inspect_orbit_txt_health(source_path))
|
||||||
|
sample["has_corruption_signal"] = _has_orbit_corruption_signal(sample)
|
||||||
|
if sample.get("read_error"):
|
||||||
|
sample["error"] = f"Source health scan failed: {sample['read_error']}"
|
||||||
|
elif sample["has_corruption_signal"]:
|
||||||
|
sample["error"] = "Source TXT contains NUL bytes and appears corrupted"
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
def validate_orbit_source(txt_path: str, stem: str = "", scratch_root: str = "") -> Dict[str, Any]:
|
def validate_orbit_source(txt_path: str, stem: str = "", scratch_root: str = "") -> Dict[str, Any]:
|
||||||
name = stem or os.path.splitext(os.path.basename(txt_path))[0]
|
name = stem or os.path.splitext(os.path.basename(txt_path))[0]
|
||||||
try:
|
try:
|
||||||
@@ -259,28 +330,42 @@ def summarize_source_orbit_gaps(
|
|||||||
source_without_envi = sorted(source_stems - envi_stems)
|
source_without_envi = sorted(source_stems - envi_stems)
|
||||||
envi_without_source = sorted(envi_stems - source_stems)
|
envi_without_source = sorted(envi_stems - source_stems)
|
||||||
isce2_without_source = sorted(isce2_stems - source_stems)
|
isce2_without_source = sorted(isce2_stems - source_stems)
|
||||||
|
sample_limit = 20
|
||||||
|
|
||||||
def _make_samples(stems: List[str]) -> List[Dict[str, str]]:
|
def _make_samples(
|
||||||
samples: List[Dict[str, str]] = []
|
stems: List[str],
|
||||||
for stem in stems[:20]:
|
inspect_source: bool = False,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
samples: List[Dict[str, Any]] = []
|
||||||
|
for stem in stems[:sample_limit]:
|
||||||
samples.append(
|
samples.append(
|
||||||
{
|
_build_source_gap_sample(
|
||||||
"name": stem,
|
stem,
|
||||||
"source_path": source_files.get(stem, {}).get("path", ""),
|
source_files.get(stem),
|
||||||
"envi_path": envi_files.get(stem, {}).get("path", ""),
|
envi_files.get(stem),
|
||||||
"isce2_path": isce2_files.get(stem, {}).get("path", ""),
|
isce2_files.get(stem),
|
||||||
}
|
inspect_source=inspect_source,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return samples
|
return samples
|
||||||
|
|
||||||
|
suspect_bad_samples = _make_samples(source_without_isce2, inspect_source=True)
|
||||||
|
bad_source_samples = [
|
||||||
|
item for item in suspect_bad_samples
|
||||||
|
if item.get("has_corruption_signal")
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"sample_limit": sample_limit,
|
||||||
"quarantine_path": _default_quarantine_root(source_dir, quarantine_root),
|
"quarantine_path": _default_quarantine_root(source_dir, quarantine_root),
|
||||||
"source_without_isce2_count": len(source_without_isce2),
|
"source_without_isce2_count": len(source_without_isce2),
|
||||||
"source_without_envi_count": len(source_without_envi),
|
"source_without_envi_count": len(source_without_envi),
|
||||||
"envi_without_source_count": len(envi_without_source),
|
"envi_without_source_count": len(envi_without_source),
|
||||||
"isce2_without_source_count": len(isce2_without_source),
|
"isce2_without_source_count": len(isce2_without_source),
|
||||||
"suspect_bad_count": len(source_without_isce2),
|
"suspect_bad_count": len(source_without_isce2),
|
||||||
"suspect_bad_samples": _make_samples(source_without_isce2),
|
"suspect_bad_samples": suspect_bad_samples,
|
||||||
|
"bad_source_sample_count": len(bad_source_samples),
|
||||||
|
"bad_source_samples": bad_source_samples,
|
||||||
"source_without_envi_samples": _make_samples(source_without_envi),
|
"source_without_envi_samples": _make_samples(source_without_envi),
|
||||||
"envi_without_source_samples": _make_samples(envi_without_source),
|
"envi_without_source_samples": _make_samples(envi_without_source),
|
||||||
"isce2_without_source_samples": _make_samples(isce2_without_source),
|
"isce2_without_source_samples": _make_samples(isce2_without_source),
|
||||||
@@ -558,7 +643,8 @@ def _convert_to_isce2_xml(
|
|||||||
timeout=60,
|
timeout=60,
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise RuntimeError(result.stderr or result.stdout or "Orbit conversion failed without output")
|
detail = result.stderr or result.stdout or "Orbit conversion failed without output"
|
||||||
|
raise RuntimeError(_compact_error_message(detail))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -857,14 +943,9 @@ def repair_orbit_pools(
|
|||||||
_convert_to_isce2_xml_file(txt_path, xml_path)
|
_convert_to_isce2_xml_file(txt_path, xml_path)
|
||||||
repaired_from_envi.append(stem)
|
repaired_from_envi.append(stem)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
repair_errors.append(
|
item = _build_invalid_source_record(stem, txt_path, exc, envi_path=txt_path, isce2_path=xml_path)
|
||||||
{
|
item["target"] = xml_path
|
||||||
"name": stem,
|
repair_errors.append(item)
|
||||||
"source": txt_path,
|
|
||||||
"target": xml_path,
|
|
||||||
"error": str(exc),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
after = check_orbit_consistency(envi_pool, isce2_pool)
|
after = check_orbit_consistency(envi_pool, isce2_pool)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -34,6 +34,67 @@ const formatMismatchList = (items, formatter) => (
|
|||||||
asArray(items).map(formatter).join('; ')
|
asArray(items).map(formatter).join('; ')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const formatIntegerText = (value) => {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed.toLocaleString() : '0';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrbitSourcePath = (item) => item?.source || item?.source_path || '';
|
||||||
|
|
||||||
|
const hasOrbitCorruptionSignal = (item) => Boolean(
|
||||||
|
item && (
|
||||||
|
item.has_corruption_signal ||
|
||||||
|
item.contains_nul_bytes ||
|
||||||
|
toNumber(item.nul_byte_count) > 0 ||
|
||||||
|
item.tail_has_nul_bytes ||
|
||||||
|
Number(item.first_nul_offset) >= 0 ||
|
||||||
|
item.read_error
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderOrbitSourceIssueDetails = (item, en, formatPathText) => {
|
||||||
|
const sourcePath = getOrbitSourcePath(item);
|
||||||
|
const nulCount = toNumber(item?.nul_byte_count);
|
||||||
|
const firstNulOffset = Number(item?.first_nul_offset);
|
||||||
|
const hasNulDetails =
|
||||||
|
item?.contains_nul_bytes ||
|
||||||
|
nulCount > 0 ||
|
||||||
|
item?.tail_has_nul_bytes ||
|
||||||
|
firstNulOffset >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{sourcePath && (
|
||||||
|
<div style={{ color: '#64748b' }}>
|
||||||
|
{en ? 'Source: ' : '源文件:'}{formatPathText(sourcePath)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item?.envi_path && (
|
||||||
|
<div style={{ color: '#64748b' }}>
|
||||||
|
{en ? 'ENVI: ' : 'ENVI:'}{formatPathText(item.envi_path)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item?.isce2_path && (
|
||||||
|
<div style={{ color: '#64748b' }}>
|
||||||
|
{en ? 'ISCE2: ' : 'ISCE2:'}{formatPathText(item.isce2_path)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasNulDetails && (
|
||||||
|
<div style={{ color: '#64748b' }}>
|
||||||
|
{en ? 'NUL bytes: ' : 'NUL 字节:'}{formatIntegerText(nulCount)}
|
||||||
|
{firstNulOffset >= 0 ? `${en ? ', first offset: ' : ',首个偏移:'}${formatIntegerText(firstNulOffset)}` : ''}
|
||||||
|
{item?.tail_has_nul_bytes ? (en ? ' (tail contains NUL)' : '(文件尾含 NUL)') : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item?.read_error && (
|
||||||
|
<div style={{ color: '#64748b' }}>
|
||||||
|
{en ? 'Health scan error: ' : '文件健康扫描异常:'}{item.read_error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const formatSourceRootRole = (role, en = false) => {
|
const formatSourceRootRole = (role, en = false) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'radar_source':
|
case 'radar_source':
|
||||||
@@ -302,6 +363,12 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
|||||||
const orbitEnviWithoutSourceCount = toNumber(orbitSource.envi_without_source_count);
|
const orbitEnviWithoutSourceCount = toNumber(orbitSource.envi_without_source_count);
|
||||||
const orbitIsce2WithoutSourceCount = toNumber(orbitSource.isce2_without_source_count);
|
const orbitIsce2WithoutSourceCount = toNumber(orbitSource.isce2_without_source_count);
|
||||||
const orbitQuarantinePath = orbitSource.quarantine_path || orbitStatus?.source_gaps?.quarantine_path;
|
const orbitQuarantinePath = orbitSource.quarantine_path || orbitStatus?.source_gaps?.quarantine_path;
|
||||||
|
const orbitBadSourceSamples = asArray(orbitSource.bad_source_samples).filter(hasOrbitCorruptionSignal);
|
||||||
|
const orbitSuspectBadSamples = asArray(orbitSource.suspect_bad_samples);
|
||||||
|
const orbitSuspectWithoutCorruptionSamples = orbitSuspectBadSamples.filter(
|
||||||
|
(item) => !orbitBadSourceSamples.some((badItem) => badItem.name === item.name)
|
||||||
|
);
|
||||||
|
const orbitBadSourceSampleCount = toNumber(orbitSource.bad_source_sample_count || orbitBadSourceSamples.length);
|
||||||
const orbitOverallHealthy = Boolean(
|
const orbitOverallHealthy = Boolean(
|
||||||
orbitStatus &&
|
orbitStatus &&
|
||||||
orbitMismatchCount === 0 &&
|
orbitMismatchCount === 0 &&
|
||||||
@@ -951,6 +1018,13 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
|||||||
: `疑似坏源 TXT(源文件存在但 ISCE2 XML 缺失):${orbitSuspectBadCount}`}
|
: `疑似坏源 TXT(源文件存在但 ISCE2 XML 缺失):${orbitSuspectBadCount}`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{orbitBadSourceSampleCount > 0 && (
|
||||||
|
<div className="health-card-note error">
|
||||||
|
{en
|
||||||
|
? `Sampled source TXT with corruption signals: ${orbitBadSourceSampleCount}`
|
||||||
|
: `抽样检测到损坏信号的源 TXT:${orbitBadSourceSampleCount}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{orbitDatabase.sample_missing_in_envi?.length > 0 && (
|
{orbitDatabase.sample_missing_in_envi?.length > 0 && (
|
||||||
<div className="health-card-note error">
|
<div className="health-card-note error">
|
||||||
{en ? 'DB expected but ENVI pool missing: ' : '数据库期望但 ENVI 池缺失:'}
|
{en ? 'DB expected but ENVI pool missing: ' : '数据库期望但 ENVI 池缺失:'}
|
||||||
@@ -963,19 +1037,16 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
|||||||
{orbitDatabase.sample_missing_in_isce2.slice(0, 5).join(', ')}
|
{orbitDatabase.sample_missing_in_isce2.slice(0, 5).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(orbitSource.suspect_bad_samples || []).slice(0, 5).map((item) => (
|
{orbitBadSourceSamples.slice(0, 5).map((item) => (
|
||||||
|
<div key={`orbit-bad-source-${item.name}`} className="health-card-note error">
|
||||||
|
{item.name} - {item.error || (en ? 'Corruption signal detected' : '检测到损坏信号')}
|
||||||
|
{renderOrbitSourceIssueDetails(item, en, formatPathText)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{orbitSuspectWithoutCorruptionSamples.slice(0, 5).map((item) => (
|
||||||
<div key={`orbit-suspect-bad-${item.name}`} className="health-card-note warn">
|
<div key={`orbit-suspect-bad-${item.name}`} className="health-card-note warn">
|
||||||
{item.name}
|
{item.name}
|
||||||
{item.source_path && (
|
{renderOrbitSourceIssueDetails(item, en, formatPathText)}
|
||||||
<div style={{ color: '#64748b' }}>
|
|
||||||
{en ? 'Source: ' : '源文件:'}{formatPathText(item.source_path)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.envi_path && (
|
|
||||||
<div style={{ color: '#64748b' }}>
|
|
||||||
{en ? 'ENVI: ' : 'ENVI:'}{formatPathText(item.envi_path)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(orbitConsistency.mismatches || []).slice(0, 5).map((item) => (
|
{(orbitConsistency.mismatches || []).slice(0, 5).map((item) => (
|
||||||
@@ -1102,13 +1173,7 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
|||||||
{(orbitSyncResult.confirmed_bad || []).slice(0, 5).map((item, index) => (
|
{(orbitSyncResult.confirmed_bad || []).slice(0, 5).map((item, index) => (
|
||||||
<div key={`orbit-quarantine-bad-${index}`} className="health-card-note error">
|
<div key={`orbit-quarantine-bad-${index}`} className="health-card-note error">
|
||||||
{item.name} - {item.error}
|
{item.name} - {item.error}
|
||||||
<div style={{ color: '#64748b' }}>
|
{renderOrbitSourceIssueDetails(item, en, formatPathText)}
|
||||||
{en ? 'Source: ' : '源文件:'}{formatPathText(item.source)}
|
|
||||||
</div>
|
|
||||||
<div style={{ color: '#64748b' }}>
|
|
||||||
{en ? 'NUL bytes: ' : 'NUL 字节:'}{toNumber(item.nul_byte_count)}
|
|
||||||
{item.tail_has_nul_bytes ? (en ? ' (tail contains NUL)' : '(文件尾含 NUL)') : ''}
|
|
||||||
</div>
|
|
||||||
{item.quarantined_source && (
|
{item.quarantined_source && (
|
||||||
<div style={{ color: '#64748b' }}>
|
<div style={{ color: '#64748b' }}>
|
||||||
{en ? 'Moved source to: ' : '源文件已移至:'}{formatPathText(item.quarantined_source)}
|
{en ? 'Moved source to: ' : '源文件已移至:'}{formatPathText(item.quarantined_source)}
|
||||||
@@ -1163,9 +1228,7 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
|||||||
{(orbitSyncResult.repair_errors || []).slice(0, 3).map((item, index) => (
|
{(orbitSyncResult.repair_errors || []).slice(0, 3).map((item, index) => (
|
||||||
<div key={`orbit-repair-error-${index}`} className="health-card-note error">
|
<div key={`orbit-repair-error-${index}`} className="health-card-note error">
|
||||||
{item.name} - {item.error}
|
{item.name} - {item.error}
|
||||||
<div style={{ color: '#64748b' }}>
|
{renderOrbitSourceIssueDetails(item, en, formatPathText)}
|
||||||
{en ? 'Source: ' : '源文件:'}{formatPathText(item.source)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(orbitSyncResult.sync_result?.source?.errors || []).slice(0, 3).map((item, index) => (
|
{(orbitSyncResult.sync_result?.source?.errors || []).slice(0, 3).map((item, index) => (
|
||||||
@@ -1176,9 +1239,7 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
|||||||
{(orbitSyncResult.sync_result?.invalid_sources || []).slice(0, 5).map((item, index) => (
|
{(orbitSyncResult.sync_result?.invalid_sources || []).slice(0, 5).map((item, index) => (
|
||||||
<div key={`orbit-repair-invalid-${index}`} className="health-card-note error">
|
<div key={`orbit-repair-invalid-${index}`} className="health-card-note error">
|
||||||
{item.name} - {item.error}
|
{item.name} - {item.error}
|
||||||
<div style={{ color: '#64748b' }}>
|
{renderOrbitSourceIssueDetails(item, en, formatPathText)}
|
||||||
{en ? 'Source: ' : '源文件:'}{formatPathText(item.source)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(orbitSyncResult.sync_result?.isce2?.errors || []).slice(0, 3).map((item, index) => (
|
{(orbitSyncResult.sync_result?.isce2?.errors || []).slice(0, 3).map((item, index) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user