Load full artifact scan in dashboard

This commit is contained in:
2026-06-30 15:46:04 +08:00
parent 41cb0235cd
commit e766e4ed26
5 changed files with 61 additions and 20 deletions

View File

@@ -57,9 +57,9 @@ written under `var/custom_yolo_runs` and are scanned by the results dashboard.
When a dataset is selected, the dataset panel shows its custom YOLO `best.pt`, When a dataset is selected, the dataset panel shows its custom YOLO `best.pt`,
prediction previews, heatmap previews, and inline training curve previews. prediction previews, heatmap previews, and inline training curve previews.
Segmentation previews, YOLO heatmaps, and loss/metric artifacts are grouped on Segmentation previews, YOLO heatmaps, and loss/metric artifacts are grouped on
the results dashboard. The artifact browser can filter by model family and the results dashboard. The artifact browser loads the full result scan, can
artifact role, and YOLO-style `results.csv` files are parsed into lightweight filter by model family and artifact role, and YOLO-style `results.csv` files
training curves. are parsed into lightweight training curves.
Job APIs and the SSE log stream also expose structured progress parsed from Job APIs and the SSE log stream also expose structured progress parsed from
YOLO, MMSeg/MMEngine, SegModel-style epoch logs, and generic tqdm percentages, YOLO, MMSeg/MMEngine, SegModel-style epoch logs, and generic tqdm percentages,
so the queue and live log panel can show stage, epoch/iteration, and percent so the queue and live log panel can show stage, epoch/iteration, and percent
@@ -163,8 +163,10 @@ Use `GET /api/coverage` to inspect script-to-task coverage and task
buildability. buildability.
Use `GET /api/capabilities` to inspect the grouped full-function readiness Use `GET /api/capabilities` to inspect the grouped full-function readiness
matrix used by the web dashboard and agents. matrix used by the web dashboard and agents.
Use `GET /api/results/curves` to inspect parsed training curves discovered Use `GET /api/results?limit=1000` to inspect browsable artifacts and
from YOLO, SegModel, MMSeg, visual-tool, and analysis output directories. `GET /api/results/curves?limit=100` to inspect parsed training curves
discovered from YOLO, SegModel, MMSeg, visual-tool, and analysis output
directories.
Use `GET /api/agents/evaluate` and `GET /api/agents/validate` to surface the Use `GET /api/agents/evaluate` and `GET /api/agents/validate` to surface the
same evaluation and validation feedback shown in the web dashboard Agent panel. same evaluation and validation feedback shown in the web dashboard Agent panel.

View File

@@ -54,7 +54,10 @@ def evaluate_project() -> dict:
and "resultFamilyFilter" in frontend_text and "resultFamilyFilter" in frontend_text
and "resultRoleFilter" in frontend_text and "resultRoleFilter" in frontend_text
and "familyOptions" in frontend_text and "familyOptions" in frontend_text
and "roleOptions" in frontend_text, and "roleOptions" in frontend_text
and "/api/results?limit=1000" in frontend_text
and "setResults(resultsNext)" in frontend_text
and "slice(0, 240)" not in frontend_text,
"job_progress_ui": "JobProgressBar" in frontend_text and "progressTrack" in frontend_text, "job_progress_ui": "JobProgressBar" in frontend_text and "progressTrack" in frontend_text,
"runtime_readiness_ui": "runtimeReadiness" in frontend_text and "环境就绪" in frontend_text, "runtime_readiness_ui": "runtimeReadiness" in frontend_text and "环境就绪" in frontend_text,
"capability_matrix_ui": "capabilities" in frontend_text and "全功能矩阵" in frontend_text, "capability_matrix_ui": "capabilities" in frontend_text and "全功能矩阵" in frontend_text,

View File

@@ -4,7 +4,7 @@ import asyncio
import json import json
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi import FastAPI, File, HTTPException, Query, UploadFile
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
@@ -211,12 +211,12 @@ async def api_job_events(job_id: str):
@app.get("/api/results") @app.get("/api/results")
def api_results() -> list[dict]: def api_results(limit: int = Query(1000, ge=1, le=5000)) -> list[dict]:
return scan_results() return scan_results(limit=limit)
@app.get("/api/results/curves") @app.get("/api/results/curves")
def api_result_curves(limit: int = 100) -> list[dict]: def api_result_curves(limit: int = Query(100, ge=1, le=500)) -> list[dict]:
return scan_training_curves(limit=limit) return scan_training_curves(limit=limit)

View File

@@ -1,5 +1,9 @@
from pathlib import Path from pathlib import Path
from fastapi.testclient import TestClient
from app import main
from app.main import app
from app.modules.results.service import parse_training_curve from app.modules.results.service import parse_training_curve
@@ -25,3 +29,31 @@ def test_parse_yolo_results_curve(tmp_path: Path):
assert "train/seg_loss" in names assert "train/seg_loss" in names
assert "metrics/mAP50(M)" in names assert "metrics/mAP50(M)" in names
assert all("lr/" not in name for name in names) assert all("lr/" not in name for name in names)
def test_results_api_honors_limit(monkeypatch):
calls = {}
def fake_scan_results(limit: int = 1000):
calls["limit"] = limit
return [{"name": "a.png", "relative_path": "var/a.png"}]
monkeypatch.setattr(main, "scan_results", fake_scan_results)
response = TestClient(app).get("/api/results?limit=123")
assert response.status_code == 200
assert calls["limit"] == 123
def test_results_curve_api_honors_limit(monkeypatch):
calls = {}
def fake_scan_training_curves(limit: int = 100):
calls["limit"] = limit
return [{"name": "results", "relative_path": "var/results.csv"}]
monkeypatch.setattr(main, "scan_training_curves", fake_scan_training_curves)
response = TestClient(app).get("/api/results/curves?limit=77")
assert response.status_code == 200
assert calls["limit"] == 77

View File

@@ -347,8 +347,8 @@ function useData() {
api<RuntimeReadinessPayload>("/api/system/readiness"), api<RuntimeReadinessPayload>("/api/system/readiness"),
api<CapabilityPayload>("/api/capabilities"), api<CapabilityPayload>("/api/capabilities"),
api<Job[]>("/api/jobs"), api<Job[]>("/api/jobs"),
api<ResultItem[]>("/api/results"), api<ResultItem[]>("/api/results?limit=1000"),
api<TrainingCurve[]>("/api/results/curves"), api<TrainingCurve[]>("/api/results/curves?limit=100"),
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"),
@@ -360,8 +360,8 @@ function useData() {
setRuntimeReadiness(readinessNext); setRuntimeReadiness(readinessNext);
setCapabilities(capabilitiesNext); setCapabilities(capabilitiesNext);
setJobs(jobsNext); setJobs(jobsNext);
setResults(resultsNext.slice(0, 240)); setResults(resultsNext);
setCurves(curvesNext.slice(0, 12)); setCurves(curvesNext);
setDatasets(datasetsNext); setDatasets(datasetsNext);
const validationEntries: Array<[string, DatasetValidation]> = []; const validationEntries: Array<[string, DatasetValidation]> = [];
await Promise.all( await Promise.all(
@@ -1319,12 +1319,16 @@ function ResultBrowser({
<span>/ {total} artifacts</span> <span>/ {total} artifacts</span>
</div> </div>
<div className="resultList"> <div className="resultList">
{results.map((item) => ( {results.length ? (
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer"> results.map((item) => (
<span>{item.name}</span> <a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
<small>{item.family ?? "artifact"} · {item.role ?? "artifact"} · {formatBytes(item.size)}</small> <span>{item.name}</span>
</a> <small>{item.family ?? "artifact"} · {item.role ?? "artifact"} · {formatBytes(item.size)}</small>
))} </a>
))
) : (
<p className="muted"></p>
)}
</div> </div>
</div> </div>
); );