Add deep training acceptance checks
This commit is contained in:
@@ -7,4 +7,5 @@ SEG_MMSEG_CONDA_ENV=seg_mmcv
|
|||||||
SEG_BACKEND_CONDA_ENV=seg_smp
|
SEG_BACKEND_CONDA_ENV=seg_smp
|
||||||
SEG_WEIGHT_MODE=copy
|
SEG_WEIGHT_MODE=copy
|
||||||
SEG_ENABLE_SHELL_TASKS=1
|
SEG_ENABLE_SHELL_TASKS=1
|
||||||
|
SEG_VALIDATE_DEEP=1
|
||||||
VITE_API_BASE=http://localhost:8010
|
VITE_API_BASE=http://localhost:8010
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ weight discovery. MMSeg full-model readiness is validated in
|
|||||||
`SEG_MMSEG_CONDA_ENV` by importing `mmcv._ext` and building a local MMSeg
|
`SEG_MMSEG_CONDA_ENV` by importing `mmcv._ext` and building a local MMSeg
|
||||||
`EncoderDecoder` from the existing config tree.
|
`EncoderDecoder` from the existing config tree.
|
||||||
|
|
||||||
|
For stronger runtime proof, `POST /api/acceptance/deep` runs minimal training
|
||||||
|
loops for the three model families: one SegModel optimizer step, one YOLO
|
||||||
|
segmentation epoch on a synthetic 64x64 dataset, and one MMSeg optimizer step
|
||||||
|
through the full `mmcv._ext` runtime. The latest report is available from
|
||||||
|
`GET /api/acceptance/deep/latest` and is surfaced in the coverage panel.
|
||||||
|
|
||||||
Current `seg_smp` uses `mmcv-lite` because no `torch 2.6/cu124` full `mmcv`
|
Current `seg_smp` uses `mmcv-lite` because no `torch 2.6/cu124` full `mmcv`
|
||||||
wheel is available on this machine and `nvcc` is not installed for source
|
wheel is available on this machine and `nvcc` is not installed for source
|
||||||
builds. A dedicated `seg_mmcv` environment is used for MMSeg tasks and has
|
builds. A dedicated `seg_mmcv` environment is used for MMSeg tasks and has
|
||||||
@@ -138,4 +144,5 @@ The validation agent checks catalog coverage, the `seg_smp` task env, the
|
|||||||
`seg_mmcv` MMSeg env, GPU visibility, no-weight Git safety, backend tests,
|
`seg_mmcv` MMSeg env, GPU visibility, no-weight Git safety, backend tests,
|
||||||
frontend build, and live backend/frontend endpoints when the services are
|
frontend build, and live backend/frontend endpoints when the services are
|
||||||
running. With live validation enabled it also runs the lightweight acceptance
|
running. With live validation enabled it also runs the lightweight acceptance
|
||||||
smoke above.
|
smoke above. By default it also runs the deep training acceptance; set
|
||||||
|
`SEG_VALIDATE_DEEP=0` when a quick non-training validation pass is needed.
|
||||||
|
|||||||
@@ -62,6 +62,66 @@ MMSEG_FULL_BUILD_SNIPPET = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SEGMODEL_TRAIN_STEP_SNIPPET = (
|
||||||
|
"import torch, segmentation_models_pytorch as smp; "
|
||||||
|
"torch.manual_seed(7); "
|
||||||
|
"model=smp.Unet(encoder_name='resnet18', encoder_weights=None, classes=2).train(); "
|
||||||
|
"inputs=torch.randn(2,3,64,64); "
|
||||||
|
"targets=torch.randint(0,2,(2,64,64)); "
|
||||||
|
"optimizer=torch.optim.SGD(model.parameters(), lr=1e-3); "
|
||||||
|
"outputs=model(inputs); "
|
||||||
|
"loss=torch.nn.functional.cross_entropy(outputs, targets); "
|
||||||
|
"loss.backward(); optimizer.step(); "
|
||||||
|
"print('loss', round(float(loss.detach()), 6), 'shape', tuple(outputs.shape))"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _yolo_tiny_train_snippet(root: Path, weight: Path) -> str:
|
||||||
|
return (
|
||||||
|
"import shutil, cv2, numpy as np; "
|
||||||
|
"from pathlib import Path; "
|
||||||
|
"from ultralytics import YOLO; "
|
||||||
|
f"root=Path({str(root)!r}); weight={str(weight)!r}; "
|
||||||
|
"shutil.rmtree(root, ignore_errors=True); "
|
||||||
|
"[ (root / item).mkdir(parents=True, exist_ok=True) for item in ['images/train','images/val','labels/train','labels/val','runs'] ]; "
|
||||||
|
"image=np.zeros((64,64,3), dtype=np.uint8); "
|
||||||
|
"cv2.rectangle(image, (16,16), (48,48), (255,255,255), -1); "
|
||||||
|
"label='0 0.25 0.25 0.75 0.25 0.75 0.75 0.25 0.75\\n'; "
|
||||||
|
"\nfor split in ['train','val']:\n"
|
||||||
|
" cv2.imwrite(str(root / 'images' / split / 'sample.jpg'), image)\n"
|
||||||
|
" (root / 'labels' / split / 'sample.txt').write_text(label, encoding='utf-8')\n"
|
||||||
|
"(root / 'data.yaml').write_text('path: '+str(root)+'\\ntrain: images/train\\nval: images/val\\nnc: 1\\nnames:\\n 0: object\\n', encoding='utf-8'); "
|
||||||
|
"model=YOLO(weight); "
|
||||||
|
"model.train(data=str(root/'data.yaml'), epochs=1, imgsz=64, batch=1, workers=0, device='cpu', project=str(root/'runs'), name='tiny', exist_ok=True, verbose=False, plots=False, val=False); "
|
||||||
|
"results=root/'runs'/'tiny'/'results.csv'; best=root/'runs'/'tiny'/'weights'/'best.pt'; "
|
||||||
|
"assert results.exists() and results.stat().st_size > 0; "
|
||||||
|
"assert best.exists() and best.stat().st_size > 0; "
|
||||||
|
"print('results', results, results.stat().st_size, 'best', best.stat().st_size)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mmseg_train_step_snippet(config_path: Path) -> str:
|
||||||
|
return (
|
||||||
|
"import torch; "
|
||||||
|
"from mmengine.config import Config; "
|
||||||
|
"from mmengine.structures import PixelData; "
|
||||||
|
"from mmseg.registry import MODELS; "
|
||||||
|
"from mmseg.structures import SegDataSample; "
|
||||||
|
"from mmseg.utils import register_all_modules; "
|
||||||
|
"register_all_modules(init_default_scope=True); "
|
||||||
|
f"cfg=Config.fromfile({str(config_path)!r}); "
|
||||||
|
"cfg.model.backbone.init_cfg=None; cfg.model.pretrained=None; "
|
||||||
|
"model=MODELS.build(cfg.model).train(); "
|
||||||
|
"sample=SegDataSample(); "
|
||||||
|
"sample.gt_sem_seg=PixelData(data=torch.randint(0,19,(1,64,64), dtype=torch.long)); "
|
||||||
|
"losses=model(torch.randn(1,3,64,64), [sample], mode='loss'); "
|
||||||
|
"loss=sum(value if torch.is_tensor(value) else sum(value) for value in losses.values()); "
|
||||||
|
"optimizer=torch.optim.SGD(model.parameters(), lr=1e-4); "
|
||||||
|
"loss.backward(); optimizer.step(); "
|
||||||
|
"print('loss', round(float(loss.detach()), 6), sorted(losses.keys()))"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _request_json(method: str, url: str, payload: dict[str, Any] | None = None, timeout: int = 10) -> dict[str, Any]:
|
def _request_json(method: str, url: str, payload: dict[str, Any] | None = None, timeout: int = 10) -> dict[str, Any]:
|
||||||
data = None
|
data = None
|
||||||
headers = {"Accept": "application/json"}
|
headers = {"Accept": "application/json"}
|
||||||
@@ -252,6 +312,56 @@ def latest_acceptance_report() -> dict[str, Any]:
|
|||||||
return json.loads(path.read_text(encoding="utf-8"))
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def latest_deep_acceptance_report() -> dict[str, Any]:
|
||||||
|
path = settings.project_root / "var" / "acceptance" / "deep_latest.json"
|
||||||
|
if not path.exists():
|
||||||
|
return {"available": False, "path": str(path)}
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def run_deep_acceptance() -> dict[str, Any]:
|
||||||
|
"""Run minimal training loops for each model family without full datasets."""
|
||||||
|
acceptance_root = settings.project_root / "var" / "acceptance"
|
||||||
|
run_id = uuid.uuid4().hex[:8]
|
||||||
|
fixture_root = acceptance_root / f"deep_{run_id}"
|
||||||
|
fixture_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
yolo_weight = settings.source_root / "Seg_All_In_One_YoloModel" / "yolo11n-seg.pt"
|
||||||
|
mmseg_config = settings.source_root / "Seg_All_In_One_MMSeg" / "configs" / "fcn" / "fcn_r18-d8_4xb2-80k_cityscapes-512x1024.py"
|
||||||
|
|
||||||
|
checks = [
|
||||||
|
{
|
||||||
|
"name": "segmodel_tiny_train_step",
|
||||||
|
"passed": False,
|
||||||
|
"detail": _run_snippet(SEGMODEL_TRAIN_STEP_SNIPPET, timeout=90),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yolo_tiny_segment_train_epoch",
|
||||||
|
"passed": False,
|
||||||
|
"detail": _run_snippet(_yolo_tiny_train_snippet(fixture_root / "yolo_tiny", yolo_weight), timeout=180),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mmseg_tiny_train_step",
|
||||||
|
"passed": False,
|
||||||
|
"detail": _run_conda_snippet(settings.mmseg_conda_env, _mmseg_train_step_snippet(mmseg_config), timeout=120),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for check in checks:
|
||||||
|
check["passed"] = bool(check["detail"].get("passed"))
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"available": True,
|
||||||
|
"run_id": run_id,
|
||||||
|
"fixture_root": str(fixture_root),
|
||||||
|
"passed": all(item["passed"] for item in checks),
|
||||||
|
"checks": checks,
|
||||||
|
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
}
|
||||||
|
latest = acceptance_root / "deep_latest.json"
|
||||||
|
latest.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
def run_live_acceptance(base_url: str = "http://127.0.0.1:8010") -> dict[str, Any]:
|
def run_live_acceptance(base_url: str = "http://127.0.0.1:8010") -> dict[str, Any]:
|
||||||
"""Run a lightweight end-to-end smoke against the live API and job runner."""
|
"""Run a lightweight end-to-end smoke against the live API and job runner."""
|
||||||
acceptance_root = settings.project_root / "var" / "acceptance"
|
acceptance_root = settings.project_root / "var" / "acceptance"
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ def evaluate_project() -> dict:
|
|||||||
"loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" in frontend_text,
|
"loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" in frontend_text,
|
||||||
"dataset_api": "/api/datasets" in backend_text and "api_upload_dataset_files" in backend_text,
|
"dataset_api": "/api/datasets" in backend_text and "api_upload_dataset_files" in backend_text,
|
||||||
"curve_api": "/api/results/curves" in backend_text,
|
"curve_api": "/api/results/curves" in backend_text,
|
||||||
|
"deep_acceptance_api": "/api/acceptance/deep" in backend_text,
|
||||||
|
"deep_acceptance_ui": "runDeepAcceptance" in frontend_text and "深度训练" in frontend_text,
|
||||||
"coverage_api": "/api/coverage" in backend_text and coverage["task_build_passed"],
|
"coverage_api": "/api/coverage" in backend_text and coverage["task_build_passed"],
|
||||||
"visual_tools": "visual.yolo11_heatmap_v2" in catalog["task_types"] and "visual.fps" in catalog["task_types"],
|
"visual_tools": "visual.yolo11_heatmap_v2" in catalog["task_types"] and "visual.fps" in catalog["task_types"],
|
||||||
"yolo_dataset_tools": "dataset.yolo_txt_sort" in catalog["task_types"] and "dataset.yolo_resize" in catalog["task_types"],
|
"yolo_dataset_tools": "dataset.yolo_txt_sort" in catalog["task_types"] and "dataset.yolo_resize" in catalog["task_types"],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import urllib.error
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..acceptance import run_live_acceptance
|
from ..acceptance import run_deep_acceptance, run_live_acceptance
|
||||||
from ..catalog import get_catalog
|
from ..catalog import get_catalog
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..coverage import get_coverage_report
|
from ..coverage import get_coverage_report
|
||||||
@@ -108,6 +108,9 @@ def validate_project(run_build: bool = False) -> dict:
|
|||||||
if os.getenv("SEG_VALIDATE_ACCEPTANCE", "1") == "1":
|
if os.getenv("SEG_VALIDATE_ACCEPTANCE", "1") == "1":
|
||||||
acceptance = run_live_acceptance(backend_url)
|
acceptance = run_live_acceptance(backend_url)
|
||||||
checks.append({"name": "live_acceptance_smoke", "passed": acceptance["passed"], "detail": acceptance})
|
checks.append({"name": "live_acceptance_smoke", "passed": acceptance["passed"], "detail": acceptance})
|
||||||
|
if os.getenv("SEG_VALIDATE_DEEP", "1") == "1":
|
||||||
|
deep_acceptance = run_deep_acceptance()
|
||||||
|
checks.append({"name": "deep_training_acceptance", "passed": deep_acceptance["passed"], "detail": deep_acceptance})
|
||||||
|
|
||||||
if run_build:
|
if run_build:
|
||||||
tests = _run(["conda", "run", "-n", settings.backend_conda_env, "python", "-m", "pytest", "-q"], cwd=settings.project_root, timeout=120)
|
tests = _run(["conda", "run", "-n", settings.backend_conda_env, "python", "-m", "pytest", "-q"], cwd=settings.project_root, timeout=120)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .acceptance import latest_acceptance_report, run_live_acceptance
|
from .acceptance import latest_acceptance_report, latest_deep_acceptance_report, run_deep_acceptance, run_live_acceptance
|
||||||
from .catalog import get_catalog
|
from .catalog import get_catalog
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .coverage import get_coverage_report
|
from .coverage import get_coverage_report
|
||||||
@@ -81,6 +81,16 @@ def api_acceptance_smoke(base_url: str = "http://127.0.0.1:8010") -> dict:
|
|||||||
return run_live_acceptance(base_url)
|
return run_live_acceptance(base_url)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/acceptance/deep/latest")
|
||||||
|
def api_deep_acceptance_latest() -> dict:
|
||||||
|
return latest_deep_acceptance_report()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/acceptance/deep")
|
||||||
|
def api_deep_acceptance() -> dict:
|
||||||
|
return run_deep_acceptance()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/datasets")
|
@app.get("/api/datasets")
|
||||||
def api_datasets() -> list[dict]:
|
def api_datasets() -> list[dict]:
|
||||||
return list_uploaded_datasets()
|
return list_uploaded_datasets()
|
||||||
|
|||||||
@@ -109,6 +109,14 @@ type AcceptancePayload = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DeepAcceptancePayload = {
|
||||||
|
available?: boolean;
|
||||||
|
passed?: boolean;
|
||||||
|
run_id?: string;
|
||||||
|
created_at?: string;
|
||||||
|
checks?: Array<{ name: string; passed: boolean }>;
|
||||||
|
};
|
||||||
|
|
||||||
type GpuPayload = {
|
type GpuPayload = {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
gpus: Array<{
|
gpus: Array<{
|
||||||
@@ -191,11 +199,12 @@ function useData() {
|
|||||||
const [datasets, setDatasets] = useState<UploadedDataset[]>([]);
|
const [datasets, setDatasets] = useState<UploadedDataset[]>([]);
|
||||||
const [coverage, setCoverage] = useState<CoveragePayload | null>(null);
|
const [coverage, setCoverage] = useState<CoveragePayload | null>(null);
|
||||||
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
|
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
|
||||||
|
const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const [catalogNext, gpusNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext] = await Promise.all([
|
const [catalogNext, gpusNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext] = await Promise.all([
|
||||||
api<Catalog>("/api/catalog"),
|
api<Catalog>("/api/catalog"),
|
||||||
api<GpuPayload>("/api/system/gpus"),
|
api<GpuPayload>("/api/system/gpus"),
|
||||||
api<Job[]>("/api/jobs"),
|
api<Job[]>("/api/jobs"),
|
||||||
@@ -203,7 +212,8 @@ function useData() {
|
|||||||
api<TrainingCurve[]>("/api/results/curves"),
|
api<TrainingCurve[]>("/api/results/curves"),
|
||||||
api<UploadedDataset[]>("/api/datasets"),
|
api<UploadedDataset[]>("/api/datasets"),
|
||||||
api<CoveragePayload>("/api/coverage"),
|
api<CoveragePayload>("/api/coverage"),
|
||||||
api<AcceptancePayload>("/api/acceptance/latest")
|
api<AcceptancePayload>("/api/acceptance/latest"),
|
||||||
|
api<DeepAcceptancePayload>("/api/acceptance/deep/latest")
|
||||||
]);
|
]);
|
||||||
setCatalog(catalogNext);
|
setCatalog(catalogNext);
|
||||||
setGpus(gpusNext);
|
setGpus(gpusNext);
|
||||||
@@ -213,6 +223,7 @@ function useData() {
|
|||||||
setDatasets(datasetsNext);
|
setDatasets(datasetsNext);
|
||||||
setCoverage(coverageNext);
|
setCoverage(coverageNext);
|
||||||
setAcceptance(acceptanceNext);
|
setAcceptance(acceptanceNext);
|
||||||
|
setDeepAcceptance(deepAcceptanceNext);
|
||||||
setError("");
|
setError("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(String(err));
|
setError(String(err));
|
||||||
@@ -225,7 +236,7 @@ function useData() {
|
|||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, error, refresh };
|
return { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, deepAcceptance, error, refresh };
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusPill({ status }: { status: string }) {
|
function StatusPill({ status }: { status: string }) {
|
||||||
@@ -233,7 +244,7 @@ function StatusPill({ status }: { status: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, error, refresh } = useData();
|
const { catalog, gpus, jobs, results, curves, datasets, coverage, acceptance, deepAcceptance, error, refresh } = useData();
|
||||||
const [taskType, setTaskType] = useState("mock.echo");
|
const [taskType, setTaskType] = useState("mock.echo");
|
||||||
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
||||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||||
@@ -333,6 +344,16 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runDeepAcceptance() {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await api("/api/acceptance/deep", { method: "POST" });
|
||||||
|
await refresh();
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createDataset() {
|
async function createDataset() {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
@@ -591,9 +612,14 @@ function App() {
|
|||||||
<p className="eyebrow">Coverage</p>
|
<p className="eyebrow">Coverage</p>
|
||||||
<h2>Seg 功能覆盖</h2>
|
<h2>Seg 功能覆盖</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="buttonRow compactButtons">
|
||||||
<button className="iconButton" disabled={busy} onClick={runAcceptanceSmoke} title="运行轻量验收">
|
<button className="iconButton" disabled={busy} onClick={runAcceptanceSmoke} title="运行轻量验收">
|
||||||
<ClipboardCheck size={18} />
|
<ClipboardCheck size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
<button className="iconButton" disabled={busy} onClick={runDeepAcceptance} title="运行深度训练验收">
|
||||||
|
<Activity size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="coverageGrid">
|
<div className="coverageGrid">
|
||||||
<div>
|
<div>
|
||||||
@@ -616,12 +642,17 @@ function App() {
|
|||||||
<span>模型族</span>
|
<span>模型族</span>
|
||||||
<strong>{acceptance?.model_family_readiness?.passed ? "OK" : "Check"}</strong>
|
<strong>{acceptance?.model_family_readiness?.passed ? "OK" : "Check"}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>深度训练</span>
|
||||||
|
<strong>{deepAcceptance?.available === false ? "New" : deepAcceptance?.passed ? "OK" : "Check"}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="coverageStatus">
|
<div className="coverageStatus">
|
||||||
{(coverage?.unmapped_user_scripts.length ?? 0) === 0 ? (
|
{(coverage?.unmapped_user_scripts.length ?? 0) === 0 ? (
|
||||||
<>
|
<>
|
||||||
<span>当前用户侧脚本已全部映射到网页任务。</span>
|
<span>当前用户侧脚本已全部映射到网页任务。</span>
|
||||||
<span>最近验收:{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""}</span>
|
<span>最近验收:{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""}</span>
|
||||||
|
<span>深度验收:{deepAcceptance?.created_at ?? "尚未运行"} {deepAcceptance?.run_id ? `#${deepAcceptance.run_id}` : ""},通过 {deepAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{deepAcceptance?.checks?.length ?? 0}</span>
|
||||||
<span>模型族 readiness:{acceptance?.model_family_readiness?.checks?.filter((item) => item.passed).length ?? 0}/{acceptance?.model_family_readiness?.checks?.length ?? 0},warnings {acceptance?.model_family_readiness?.warnings?.length ?? 0}</span>
|
<span>模型族 readiness:{acceptance?.model_family_readiness?.checks?.filter((item) => item.passed).length ?? 0}/{acceptance?.model_family_readiness?.checks?.length ?? 0},warnings {acceptance?.model_family_readiness?.warnings?.length ?? 0}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -358,6 +358,10 @@ textarea {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonRow.compactButtons {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.opGrid {
|
.opGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
@@ -399,7 +403,7 @@ textarea {
|
|||||||
|
|
||||||
.coverageGrid {
|
.coverageGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user