Document deployment and expose weight manifest
This commit is contained in:
@@ -130,6 +130,30 @@ type DatasetYoloOutputsPayload = {
|
||||
heatmaps: ResultItem[];
|
||||
};
|
||||
|
||||
type WeightEntry = {
|
||||
source_path: string;
|
||||
stored_path: string;
|
||||
size: number;
|
||||
family: string;
|
||||
role: string;
|
||||
sha256?: string;
|
||||
};
|
||||
|
||||
type WeightManifest = {
|
||||
generated_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
source_root?: string;
|
||||
count: number;
|
||||
total_bytes: number;
|
||||
files: WeightEntry[];
|
||||
};
|
||||
|
||||
type WeightVerifyPayload = {
|
||||
count: number;
|
||||
ok_count: number;
|
||||
items: Array<{ stored_path: string; exists: boolean; size_ok: boolean; hash_ok?: boolean | null; ok: boolean }>;
|
||||
};
|
||||
|
||||
type CoveragePayload = {
|
||||
scripts_total: number;
|
||||
user_scripts_total: number;
|
||||
@@ -344,6 +368,7 @@ function useData() {
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [results, setResults] = useState<ResultItem[]>([]);
|
||||
const [curves, setCurves] = useState<TrainingCurve[]>([]);
|
||||
const [weightManifest, setWeightManifest] = useState<WeightManifest | null>(null);
|
||||
const [datasets, setDatasets] = useState<UploadedDataset[]>([]);
|
||||
const [datasetValidations, setDatasetValidations] = useState<Record<string, DatasetValidation>>({});
|
||||
const [coverage, setCoverage] = useState<CoveragePayload | null>(null);
|
||||
@@ -356,7 +381,7 @@ function useData() {
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([
|
||||
const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([
|
||||
api<Catalog>("/api/catalog"),
|
||||
api<GpuPayload>("/api/system/gpus"),
|
||||
api<CondaEnvPayload>("/api/system/envs"),
|
||||
@@ -365,6 +390,7 @@ function useData() {
|
||||
api<Job[]>("/api/jobs"),
|
||||
api<ResultItem[]>("/api/results?limit=1000"),
|
||||
api<TrainingCurve[]>("/api/results/curves?limit=100"),
|
||||
api<WeightManifest>("/api/weights"),
|
||||
api<UploadedDataset[]>("/api/datasets"),
|
||||
api<CoveragePayload>("/api/coverage"),
|
||||
api<AcceptancePayload>("/api/acceptance/latest"),
|
||||
@@ -379,6 +405,7 @@ function useData() {
|
||||
setJobs(jobsNext);
|
||||
setResults(resultsNext);
|
||||
setCurves(curvesNext);
|
||||
setWeightManifest(weightsNext);
|
||||
setDatasets(datasetsNext);
|
||||
const validationEntries: Array<[string, DatasetValidation]> = [];
|
||||
await Promise.all(
|
||||
@@ -408,7 +435,7 @@ function useData() {
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh };
|
||||
return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh };
|
||||
}
|
||||
|
||||
function StatusPill({ status }: { status: string }) {
|
||||
@@ -431,7 +458,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
|
||||
const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
|
||||
const [taskType, setTaskType] = useState("mock.echo");
|
||||
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||
@@ -448,6 +475,7 @@ function App() {
|
||||
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
|
||||
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
|
||||
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
||||
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
||||
const [agentBusy, setAgentBusy] = useState(false);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
@@ -587,12 +615,23 @@ function App() {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ mode: "copy", hash_files: true, skip_existing: true })
|
||||
});
|
||||
setWeightVerification(null);
|
||||
await refresh();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyWeights() {
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = await api<WeightVerifyPayload>("/api/weights/verify", { method: "POST" });
|
||||
setWeightVerification(result);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function runAcceptanceSmoke() {
|
||||
setBusy(true);
|
||||
try {
|
||||
@@ -1227,20 +1266,14 @@ function App() {
|
||||
<p className="muted">{runtimeReadiness?.passed ? "runtime imports ready" : "run scripts/bootstrap_conda_envs.sh"} · {runtimeReadiness?.generated_at ?? "not checked"}</p>
|
||||
</div>
|
||||
|
||||
<div className="panel" id="weights">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Assets</p>
|
||||
<h2>权重</h2>
|
||||
</div>
|
||||
<button className="iconButton" disabled={busy} onClick={syncWeights} title="同步权重">
|
||||
<UploadCloud size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bigNumber">{catalog?.weights.count ?? 0}</div>
|
||||
<p className="muted">{formatBytes(catalog?.weights.total_bytes)} indexed</p>
|
||||
<p className="muted">{catalog?.weights.updated_at ?? "manifest not generated"}</p>
|
||||
</div>
|
||||
<WeightPanel
|
||||
catalog={catalog}
|
||||
manifest={weightManifest}
|
||||
verification={weightVerification}
|
||||
busy={busy}
|
||||
onSync={syncWeights}
|
||||
onVerify={verifyWeights}
|
||||
/>
|
||||
|
||||
<div className="panel">
|
||||
<div className="panelHead">
|
||||
@@ -1364,6 +1397,65 @@ function JobDiagnostics({ job }: { job: Job }) {
|
||||
);
|
||||
}
|
||||
|
||||
function WeightPanel({
|
||||
catalog,
|
||||
manifest,
|
||||
verification,
|
||||
busy,
|
||||
onSync,
|
||||
onVerify
|
||||
}: {
|
||||
catalog: Catalog | null;
|
||||
manifest: WeightManifest | null;
|
||||
verification: WeightVerifyPayload | null;
|
||||
busy: boolean;
|
||||
onSync: () => void;
|
||||
onVerify: () => void;
|
||||
}) {
|
||||
const files = manifest?.files ?? [];
|
||||
const familyStats = Array.from(files.reduce((counts, item) => counts.set(item.family, (counts.get(item.family) ?? 0) + 1), new Map<string, number>()).entries())
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
return (
|
||||
<div className="panel" id="weights">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Assets</p>
|
||||
<h2>权重</h2>
|
||||
</div>
|
||||
<div className="buttonRow compactButtons">
|
||||
<button className="iconButton" disabled={busy || !files.length} onClick={onVerify} title="校验权重 manifest">
|
||||
<ShieldCheck size={18} />
|
||||
</button>
|
||||
<button className="iconButton" disabled={busy} onClick={onSync} title="同步权重">
|
||||
<UploadCloud size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bigNumber">{manifest?.count ?? catalog?.weights.count ?? 0}</div>
|
||||
<p className="muted">{formatBytes(manifest?.total_bytes ?? catalog?.weights.total_bytes)} indexed</p>
|
||||
<p className="muted">{manifest?.updated_at ?? catalog?.weights.updated_at ?? "manifest not generated"}</p>
|
||||
{verification && (
|
||||
<div className={verification.ok_count === verification.count ? "weightVerify ok" : "weightVerify bad"}>
|
||||
{verification.ok_count}/{verification.count} verified
|
||||
</div>
|
||||
)}
|
||||
<div className="weightFamilies">
|
||||
{familyStats.slice(0, 4).map(([family, count]) => (
|
||||
<span key={family}>{family} {count}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="weightList">
|
||||
{files.slice(0, 6).map((item) => (
|
||||
<a key={item.stored_path} href={`${API_BASE}/api/artifacts/${item.stored_path}`} target="_blank" rel="noreferrer">
|
||||
<span>{item.source_path}</span>
|
||||
<small>{item.family} · {item.role} · {formatBytes(item.size)}</small>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentCheckList({ checks, limit }: { checks: AgentCheck[]; limit: number }) {
|
||||
if (!checks.length) {
|
||||
return <p className="muted">尚未运行验证。</p>;
|
||||
|
||||
Reference in New Issue
Block a user