Show reproducible job diagnostics

This commit is contained in:
2026-06-30 16:06:26 +08:00
parent e482651545
commit 143572825a
4 changed files with 130 additions and 0 deletions

View File

@@ -67,6 +67,8 @@ without changing the original training scripts. Starting any web job or
dataset YOLO shortcut automatically opens its live log; the SSE stream resumes dataset YOLO shortcut automatically opens its live log; the SSE stream resumes
from the current log size after the initial tail so existing lines are not from the current log size after the initial tail so existing lines are not
duplicated in the panel. duplicated in the panel.
The selected-job panel also shows reproducibility diagnostics: command, cwd,
PID, exit code, log size, error text, and the exact task params submitted.
The task builder also reads `GET /api/system/gpus` and lets an operator choose The task builder also reads `GET /api/system/gpus` and lets an operator choose
CPU or one or more GPUs before launch. Selected GPUs are passed to the backend CPU or one or more GPUs before launch. Selected GPUs are passed to the backend
as `gpus`, exported as `CUDA_VISIBLE_DEVICES`, and reflected into YOLO/visual as `gpus`, exported as `CUDA_VISIBLE_DEVICES`, and reflected into YOLO/visual

View File

@@ -61,6 +61,11 @@ def evaluate_project() -> dict:
and "setResults(resultsNext)" in frontend_text and "setResults(resultsNext)" in frontend_text
and "slice(0, 240)" not 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,
"job_diagnostics_ui": "JobDiagnostics" in frontend_text
and "jobDiagnostics" in frontend_text
and "command?.join" in frontend_text
and "job.cwd" in frontend_text
and "JSON.stringify(job.params" in frontend_text,
"gpu_task_selection_ui": "selectedGpuIds" in frontend_text "gpu_task_selection_ui": "selectedGpuIds" in frontend_text
and "gpuSelector" in frontend_text and "gpuSelector" in frontend_text
and "jobPayload" in frontend_text and "jobPayload" in frontend_text

View File

@@ -41,9 +41,15 @@ type Job = {
type: string; type: string;
status: string; status: string;
description: string; description: string;
command?: string[];
cwd?: string;
pid?: number | null;
exit_code?: number | null;
error?: string | null;
created_at: string; created_at: string;
started_at?: string; started_at?: string;
finished_at?: string; finished_at?: string;
log_path?: string;
log_tail?: string; log_tail?: string;
log_size?: number; log_size?: number;
params: Record<string, unknown>; params: Record<string, unknown>;
@@ -1298,6 +1304,7 @@ function App() {
</button> </button>
</div> </div>
{selectedJob && <JobProgressBar progress={selectedJob.progress} />} {selectedJob && <JobProgressBar progress={selectedJob.progress} />}
{selectedJob && <JobDiagnostics job={selectedJob} />}
<pre>{log || "No log selected."}</pre> <pre>{log || "No log selected."}</pre>
</div> </div>
@@ -1326,6 +1333,37 @@ function App() {
); );
} }
function JobDiagnostics({ job }: { job: Job }) {
const command = job.command?.join(" ") ?? "";
return (
<div className="jobDiagnostics">
<div className="jobDetailGrid">
<div><span>ID</span><strong>{job.id.slice(0, 12)}</strong></div>
<div><span>PID</span><strong>{job.pid ?? "-"}</strong></div>
<div><span>Exit</span><strong>{job.exit_code ?? "-"}</strong></div>
<div><span>Log</span><strong>{formatBytes(job.log_size)}</strong></div>
</div>
{job.cwd && (
<div className="jobPath">
<span>CWD</span>
<code>{job.cwd}</code>
</div>
)}
{!!job.error && <div className="jobError">{job.error}</div>}
{!!command && (
<details className="jobDetailBlock" open>
<summary>Command</summary>
<pre>{command}</pre>
</details>
)}
<details className="jobDetailBlock">
<summary>Params</summary>
<pre>{JSON.stringify(job.params ?? {}, null, 2)}</pre>
</details>
</div>
);
}
function AgentCheckList({ checks, limit }: { checks: AgentCheck[]; limit: number }) { function AgentCheckList({ checks, limit }: { checks: AgentCheck[]; limit: number }) {
if (!checks.length) { if (!checks.length) {
return <p className="muted"></p>; return <p className="muted"></p>;

View File

@@ -1346,6 +1346,90 @@ meter {
white-space: pre-wrap; white-space: pre-wrap;
} }
.jobDiagnostics {
display: grid;
gap: 9px;
margin-top: 12px;
}
.jobDetailGrid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.jobDetailGrid div {
min-width: 0;
display: grid;
gap: 3px;
padding: 8px;
border: 1px solid var(--line);
border-radius: 6px;
background: #080a08;
}
.jobDetailGrid span,
.jobPath span {
color: var(--muted);
font-size: 11px;
}
.jobDetailGrid strong {
min-width: 0;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.jobPath {
min-width: 0;
display: grid;
gap: 4px;
}
.jobPath code {
display: block;
padding: 8px;
border: 1px solid var(--line);
border-radius: 6px;
background: #080a08;
color: var(--muted);
overflow-wrap: anywhere;
}
.jobError {
padding: 8px;
border: 1px solid rgba(240, 113, 103, 0.45);
border-radius: 6px;
color: var(--red);
background: rgba(240, 113, 103, 0.08);
overflow-wrap: anywhere;
}
.jobDetailBlock {
min-width: 0;
border: 1px solid var(--line);
border-radius: 6px;
background: #080a08;
}
.jobDetailBlock summary {
cursor: pointer;
padding: 8px;
color: var(--green);
font-size: 12px;
}
.jobDetailBlock pre {
min-height: 0;
max-height: 180px;
margin: 0;
border-width: 1px 0 0;
border-radius: 0;
overflow-wrap: anywhere;
}
.curvePanel { .curvePanel {
display: grid; display: grid;
gap: 10px; gap: 10px;
@@ -1479,6 +1563,7 @@ meter {
.gpuPicker, .gpuPicker,
.taskCheckList, .taskCheckList,
.agentCheckList, .agentCheckList,
.jobDetailGrid,
.qualityStats, .qualityStats,
.qualityChecks { .qualityChecks {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));