Upload 6 files
Browse files- web/static/app.css +84 -0
- web/static/app.js +3 -8
- web/static/progress.js +8 -1
- web/static/runs.js +39 -86
web/static/app.css
CHANGED
|
@@ -1275,3 +1275,87 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
|
|
| 1275 |
.run-doc-btn .doc-icon{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-weight:900}
|
| 1276 |
@media (max-width:720px){.recovery-ladder,.recovery-meta{grid-template-columns:1fr 1fr}}
|
| 1277 |
@media (max-width:480px){.recovery-ladder,.recovery-meta{grid-template-columns:1fr}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1275 |
.run-doc-btn .doc-icon{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-weight:900}
|
| 1276 |
@media (max-width:720px){.recovery-ladder,.recovery-meta{grid-template-columns:1fr 1fr}}
|
| 1277 |
@media (max-width:480px){.recovery-ladder,.recovery-meta{grid-template-columns:1fr}}
|
| 1278 |
+
|
| 1279 |
+
/* v124: stabilization override for timeline and compact Run Explorer.
|
| 1280 |
+
Keep this block at the end so older pass-specific rules cannot reintroduce
|
| 1281 |
+
wrapping timeline dots or overflowing run badges. */
|
| 1282 |
+
@media (min-width:721px){
|
| 1283 |
+
.timeline{
|
| 1284 |
+
display:grid!important;
|
| 1285 |
+
grid-auto-flow:column!important;
|
| 1286 |
+
grid-auto-rows:auto!important;
|
| 1287 |
+
align-items:start!important;
|
| 1288 |
+
gap:0!important;
|
| 1289 |
+
overflow-x:auto!important;
|
| 1290 |
+
overflow-y:hidden!important;
|
| 1291 |
+
scroll-behavior:auto!important;
|
| 1292 |
+
scrollbar-width:thin;
|
| 1293 |
+
padding-bottom:6px!important;
|
| 1294 |
+
}
|
| 1295 |
+
.timeline .step{
|
| 1296 |
+
min-width:96px!important;
|
| 1297 |
+
max-width:none!important;
|
| 1298 |
+
}
|
| 1299 |
+
}
|
| 1300 |
+
.timeline .step{min-width:0;contain:layout paint;}
|
| 1301 |
+
.timeline .step strong{
|
| 1302 |
+
display:-webkit-box;
|
| 1303 |
+
-webkit-line-clamp:2;
|
| 1304 |
+
-webkit-box-orient:vertical;
|
| 1305 |
+
overflow:hidden;
|
| 1306 |
+
text-overflow:ellipsis;
|
| 1307 |
+
min-height:2.3em;
|
| 1308 |
+
word-break:normal;
|
| 1309 |
+
}
|
| 1310 |
+
.timeline .step small{
|
| 1311 |
+
max-width:92px;
|
| 1312 |
+
margin-left:auto;
|
| 1313 |
+
margin-right:auto;
|
| 1314 |
+
overflow:hidden;
|
| 1315 |
+
text-overflow:ellipsis;
|
| 1316 |
+
white-space:nowrap;
|
| 1317 |
+
}
|
| 1318 |
+
.step.status-running .step-dot{animation:none!important;}
|
| 1319 |
+
.step.status-changed .step-dot,.step-dot.dot-pop{animation:none!important;}
|
| 1320 |
+
|
| 1321 |
+
.run-row.run-list-item{
|
| 1322 |
+
grid-template-columns:10px minmax(0,1fr) minmax(72px,88px) 26px!important;
|
| 1323 |
+
gap:10px!important;
|
| 1324 |
+
padding:12px 12px!important;
|
| 1325 |
+
}
|
| 1326 |
+
.run-list-head{gap:6px!important;}
|
| 1327 |
+
.run-list-title{font-size:13px!important;font-weight:760!important;}
|
| 1328 |
+
.run-list-kind,
|
| 1329 |
+
.run-list-kind.validation,
|
| 1330 |
+
.run-list-kind.build{
|
| 1331 |
+
border-color:#d0d5dd!important;
|
| 1332 |
+
background:#f8fafc!important;
|
| 1333 |
+
color:#475467!important;
|
| 1334 |
+
font-size:9px!important;
|
| 1335 |
+
letter-spacing:.055em!important;
|
| 1336 |
+
padding:2px 6px!important;
|
| 1337 |
+
max-width:56px!important;
|
| 1338 |
+
overflow:hidden!important;
|
| 1339 |
+
text-overflow:ellipsis!important;
|
| 1340 |
+
white-space:nowrap!important;
|
| 1341 |
+
}
|
| 1342 |
+
.run-list-sub{font-size:11.5px!important;color:#475467!important;font-weight:560!important;}
|
| 1343 |
+
.run-list-id{font-size:10px!important;color:#98a2b3!important;}
|
| 1344 |
+
.run-list-status{min-width:0!important;width:100%;display:grid!important;justify-items:end!important;gap:4px!important;}
|
| 1345 |
+
.run-status-pill{
|
| 1346 |
+
max-width:82px!important;
|
| 1347 |
+
min-width:0!important;
|
| 1348 |
+
padding:4px 8px!important;
|
| 1349 |
+
font-size:10px!important;
|
| 1350 |
+
line-height:1!important;
|
| 1351 |
+
white-space:nowrap!important;
|
| 1352 |
+
overflow:hidden!important;
|
| 1353 |
+
text-overflow:ellipsis!important;
|
| 1354 |
+
}
|
| 1355 |
+
.run-list-status time{font-size:10.5px!important;}
|
| 1356 |
+
.run-row.run-list-item .run-delete-btn{width:26px!important;height:26px!important;font-size:14px!important;}
|
| 1357 |
+
@media (max-width:520px){
|
| 1358 |
+
.run-row.run-list-item{grid-template-columns:10px minmax(0,1fr) 26px!important;}
|
| 1359 |
+
.run-list-status{grid-column:2!important;grid-row:2!important;display:flex!important;justify-content:space-between!important;}
|
| 1360 |
+
}
|
| 1361 |
+
|
web/static/app.js
CHANGED
|
@@ -178,12 +178,9 @@ function renderValidationEndpoints(detail={}){
|
|
| 178 |
bindValidationEndpointPicker(root);
|
| 179 |
const names=validationEndpointNames(detail);
|
| 180 |
const selected=normalizeApiName(formValue('validateApi')||detail.generation_smoke?.api_name||detail.api_schema?.selected_api_name||names[0]||'');
|
| 181 |
-
const details=endpointDetails(detail,selected);
|
| 182 |
-
const signature=JSON.stringify({names:names.slice(0,12),selected,inputs:details.inputs,outputs:details.outputs,description:details.description||''});
|
| 183 |
-
if(root.dataset.signature===signature)return;
|
| 184 |
-
root.dataset.signature=signature;
|
| 185 |
if(!names.length){root.innerHTML='<div class="empty compact-empty">No Gradio endpoints discovered yet. They will appear after the live API schema step.</div>';return}
|
| 186 |
const chips=names.slice(0,12).map(name=>`<button class="endpoint-chip ${name===selected?'selected':''}" type="button" data-endpoint-name="${escapeHtml(name)}" title="Set ${escapeHtml(name)} as API name"><span>${escapeHtml(name)}</span><em>${name===selected?'selected':'set'}</em></button>`).join('');
|
|
|
|
| 187 |
const selectedLine=`<div class="endpoint-selected-row"><span>API name used for validation</span><strong>${escapeHtml(selected)}</strong><button class="secondary small" type="button" id="useSelectedEndpoint" data-endpoint-name="${escapeHtml(selected)}">Set API name</button></div>`;
|
| 188 |
const inputLine=`<div class="endpoint-io"><strong>Inputs</strong><span>${renderEndpointParamChips(details.inputs)}</span></div>`;
|
| 189 |
const outputLine=details.outputs.length?`<div class="endpoint-io"><strong>Outputs</strong><span>${renderEndpointParamChips(details.outputs)}</span></div>`:'';
|
|
@@ -452,7 +449,6 @@ async function refreshValidationProgress(){
|
|
| 452 |
try{
|
| 453 |
const p=await apiGet(`/api/runs/${encodeURIComponent(state.validationRunId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}&include_job_logs=1`);
|
| 454 |
state.validationPollErrors=0;
|
| 455 |
-
if(typeof rememberRunDetail==='function')rememberRunDetail(state.validationRunId,{...p,run_id:p.run_id||state.validationRunId},false);
|
| 456 |
renderValidationStatus(p);
|
| 457 |
const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();
|
| 458 |
if(TERMINAL_STATUSES.has(status)){stopValidationPolling();showMessage(`Validation finished: ${badgeLabel(status)}`,statusClass(status)==='success'?'success':statusClass(status)==='error'?'error':'warning');loadRuns(false)}
|
|
@@ -491,10 +487,9 @@ async function launchValidate(){const formError=validateValidationForm();if(form
|
|
| 491 |
async function refreshRunReportPreview(runId){
|
| 492 |
if(!runId)return;
|
| 493 |
try{
|
| 494 |
-
const detail=await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}
|
| 495 |
if(detail.bucket_source)state.bucketSource=detail.bucket_source;
|
| 496 |
state.selectedRunDetail=detail;
|
| 497 |
-
if(typeof rememberRunDetail==='function')rememberRunDetail(runId,detail,true);
|
| 498 |
if(typeof renderRunDetail==='function')renderRunDetail(detail);
|
| 499 |
else {
|
| 500 |
const reportText=(detail.report||'No report available.').slice(0,8000);
|
|
@@ -507,7 +502,7 @@ async function refreshRunReportPreview(runId){
|
|
| 507 |
console.warn('report refresh failed',e);
|
| 508 |
}
|
| 509 |
}
|
| 510 |
-
async function refreshProgress(){if(!state.runId||state.progressPollInFlight)return;state.progressPollInFlight=true;try{const now=Date.now();const includeLogs=!state.lastJobLogPoll||now-state.lastJobLogPoll>8000;if(includeLogs)state.lastJobLogPoll=now;const p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}${includeLogs?'&include_job_logs=1':''}`);state.activeRunProgress=p;
|
| 511 |
const smoke=p.generation_smoke||p.tests?.generation_smoke||p.summary?.generation_smoke||p.state?.generation_smoke||{};
|
| 512 |
const gate=p.inference_gate||p.summary?.inference_gate||{};
|
| 513 |
const summary=p.summary||{};
|
|
|
|
| 178 |
bindValidationEndpointPicker(root);
|
| 179 |
const names=validationEndpointNames(detail);
|
| 180 |
const selected=normalizeApiName(formValue('validateApi')||detail.generation_smoke?.api_name||detail.api_schema?.selected_api_name||names[0]||'');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
if(!names.length){root.innerHTML='<div class="empty compact-empty">No Gradio endpoints discovered yet. They will appear after the live API schema step.</div>';return}
|
| 182 |
const chips=names.slice(0,12).map(name=>`<button class="endpoint-chip ${name===selected?'selected':''}" type="button" data-endpoint-name="${escapeHtml(name)}" title="Set ${escapeHtml(name)} as API name"><span>${escapeHtml(name)}</span><em>${name===selected?'selected':'set'}</em></button>`).join('');
|
| 183 |
+
const details=endpointDetails(detail,selected);
|
| 184 |
const selectedLine=`<div class="endpoint-selected-row"><span>API name used for validation</span><strong>${escapeHtml(selected)}</strong><button class="secondary small" type="button" id="useSelectedEndpoint" data-endpoint-name="${escapeHtml(selected)}">Set API name</button></div>`;
|
| 185 |
const inputLine=`<div class="endpoint-io"><strong>Inputs</strong><span>${renderEndpointParamChips(details.inputs)}</span></div>`;
|
| 186 |
const outputLine=details.outputs.length?`<div class="endpoint-io"><strong>Outputs</strong><span>${renderEndpointParamChips(details.outputs)}</span></div>`:'';
|
|
|
|
| 449 |
try{
|
| 450 |
const p=await apiGet(`/api/runs/${encodeURIComponent(state.validationRunId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}&include_job_logs=1`);
|
| 451 |
state.validationPollErrors=0;
|
|
|
|
| 452 |
renderValidationStatus(p);
|
| 453 |
const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();
|
| 454 |
if(TERMINAL_STATUSES.has(status)){stopValidationPolling();showMessage(`Validation finished: ${badgeLabel(status)}`,statusClass(status)==='success'?'success':statusClass(status)==='error'?'error':'warning');loadRuns(false)}
|
|
|
|
| 487 |
async function refreshRunReportPreview(runId){
|
| 488 |
if(!runId)return;
|
| 489 |
try{
|
| 490 |
+
const detail=await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}`);
|
| 491 |
if(detail.bucket_source)state.bucketSource=detail.bucket_source;
|
| 492 |
state.selectedRunDetail=detail;
|
|
|
|
| 493 |
if(typeof renderRunDetail==='function')renderRunDetail(detail);
|
| 494 |
else {
|
| 495 |
const reportText=(detail.report||'No report available.').slice(0,8000);
|
|
|
|
| 502 |
console.warn('report refresh failed',e);
|
| 503 |
}
|
| 504 |
}
|
| 505 |
+
async function refreshProgress(){if(!state.runId||state.progressPollInFlight)return;state.progressPollInFlight=true;try{const now=Date.now();const includeLogs=!state.lastJobLogPoll||now-state.lastJobLogPoll>8000;if(includeLogs)state.lastJobLogPoll=now;const p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}${includeLogs?'&include_job_logs=1':''}`);state.activeRunProgress=p;state.pollErrors=0;if(p.bucket_source)state.bucketSource=p.bucket_source;renderProgress(p);renderEvents(p.events||[]);renderMetrics(p);if(p.view){setText('detailRunId',p.view.run_id||p.run_id);setText('selectedModelId',p.view.header?.model||p.summary?.model_id||'—')}else{setText('detailRunId',p.run_id||state.runId)}if(p.state){setText('detailSpace',p.state.target_space||p.summary?.target_space||'—');const mode=runImplementationMode(p.summary||{},p);setText('detailImplementationMode',mode?implementationModeLabel(mode):'—');const modeEl=$('detailImplementationMode');if(modeEl&&mode)modeEl.title=implementationModeDescription(mode);setText('detailHardware',p.state.selected_hardware||p.summary?.selected_hardware||'—')}setText('detailJobStatus',p.status||p.state?.status||p.view?.header?.raw_status||'running');setText('detailGateStatus',p.inference_gate?.status||p.summary?.gate_status||'—');setText('detailTraces',Array.isArray(p.events)?String(p.events.length)+' events':'—');setText('homeRunDetailStatus',badgeLabel(p.status||p.state?.status||p.view?.header?.status||'running'));setQuickLinks({...p.links,...p.view?.links,bucket_source:p.bucket_source,run_id:p.run_id},{...p,events:p.events||[]});saveActiveRun();const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();if(TERMINAL_STATUSES.has(status)&&state.poll){clearInterval(state.poll);state.poll=null;if(state.lastReportRefreshRunId!==state.runId){state.lastReportRefreshRunId=state.runId;refreshRunReportPreview(state.runId)}setOperation(`Run finished with status: ${badgeLabel(status)}`,status.includes('success')||status.includes('succeed')?'success':status.includes('failed')||status.includes('blocker')?'error':'warning')}if(now-(state.lastRunsRefreshFromPoll||0)>15000){state.lastRunsRefreshFromPoll=now;loadRuns(false)}}catch(e){state.pollErrors+=1;console.warn(e);setOperation(`Progress polling failed ${state.pollErrors} time${state.pollErrors===1?'':'s'}. Retrying automatically…`,'warning')}finally{state.progressPollInFlight=false}} function readRunLatencyPayload(p={}){
|
| 506 |
const smoke=p.generation_smoke||p.tests?.generation_smoke||p.summary?.generation_smoke||p.state?.generation_smoke||{};
|
| 507 |
const gate=p.inference_gate||p.summary?.inference_gate||{};
|
| 508 |
const summary=p.summary||{};
|
web/static/progress.js
CHANGED
|
@@ -302,7 +302,7 @@ function ensureTimelineStepVisible(stepKey,opts={}){
|
|
| 302 |
|
| 303 |
nextLeft=Math.max(0,Math.min(maxScroll,Math.round(nextLeft)));
|
| 304 |
if(Math.abs(nextLeft-root.scrollLeft)>2){
|
| 305 |
-
const behavior=
|
| 306 |
try{root.scrollTo({left:nextLeft,behavior});}
|
| 307 |
catch(_){root.scrollLeft=nextLeft;}
|
| 308 |
}
|
|
@@ -413,6 +413,13 @@ function renderTimeline(items,overallStatus=''){
|
|
| 413 |
if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{root.dataset.userScrolled='1'},{passive:true});}
|
| 414 |
const fullList=items||[];
|
| 415 |
const list=compactTimelineItems(fullList,overallStatus);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
const trace=timelineTraceModel(list,overallStatus);
|
| 417 |
renderTimelineTraceNote(trace);
|
| 418 |
root.classList.toggle('trace-failed',trace.mode==='failed');
|
|
|
|
| 302 |
|
| 303 |
nextLeft=Math.max(0,Math.min(maxScroll,Math.round(nextLeft)));
|
| 304 |
if(Math.abs(nextLeft-root.scrollLeft)>2){
|
| 305 |
+
const behavior='auto';
|
| 306 |
try{root.scrollTo({left:nextLeft,behavior});}
|
| 307 |
catch(_){root.scrollLeft=nextLeft;}
|
| 308 |
}
|
|
|
|
| 413 |
if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{root.dataset.userScrolled='1'},{passive:true});}
|
| 414 |
const fullList=items||[];
|
| 415 |
const list=compactTimelineItems(fullList,overallStatus);
|
| 416 |
+
// Keep the compact timeline on a single row. The number of visible groups is
|
| 417 |
+
// dynamic (repair/failure groups appear only for recovery/failures), so a
|
| 418 |
+
// fixed CSS repeat count can wrap dots onto a second line. Set the exact
|
| 419 |
+
// column count at render time for desktop/tablet layouts and let mobile CSS
|
| 420 |
+
// switch back to the vertical stacked timeline.
|
| 421 |
+
const wideTimeline=typeof window==='undefined'||!window.matchMedia||window.matchMedia('(min-width: 721px)').matches;
|
| 422 |
+
root.style.gridTemplateColumns=wideTimeline&&list.length?`repeat(${list.length}, minmax(96px, 1fr))`:'';
|
| 423 |
const trace=timelineTraceModel(list,overallStatus);
|
| 424 |
renderTimelineTraceNote(trace);
|
| 425 |
root.classList.toggle('trace-failed',trace.mode==='failed');
|
web/static/runs.js
CHANGED
|
@@ -20,7 +20,17 @@ function sortRunsNewestFirst(runs=[]){
|
|
| 20 |
}
|
| 21 |
function filteredRuns(){return (state.runsCache||[]).filter(r=>runMatchesStatus(r,state.runStatusFilter||'all')&&runMatchesQuery(r,state.runSearch||''))}
|
| 22 |
function renderRunStats(){const counts={all:state.runsCache.length,running:0,full_inference_success:0,manual_hardware_required:0,failed:0,stale:0};for(const r of state.runsCache){if(runMatchesStatus(r,'running'))counts.running+=1;if(runMatchesStatus(r,'full_inference_success'))counts.full_inference_success+=1;if(runMatchesStatus(r,'manual_hardware_required'))counts.manual_hardware_required+=1;if(runMatchesStatus(r,'failed'))counts.failed+=1;if(runMatchesStatus(r,'stale'))counts.stale+=1}document.querySelectorAll('.filter[data-status]').forEach(btn=>{const status=btn.dataset.status||'all';const label={all:'All',running:'Running',full_inference_success:'Success',manual_hardware_required:'Manual',failed:'Failed',technical_blocker:'Blockers',stale:'Stale'}[status]||status;const count=counts[status];btn.innerHTML=typeof count==='number'?`${escapeHtml(label)} <span>${count}</span>`:escapeHtml(label);btn.classList.toggle('active',status===state.runStatusFilter)})}
|
| 23 |
-
function runCardKind(run={}){return isValidationRunSummary(run)?'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
function runImplementationMode(run={},detail={}){return firstNonEmpty(run.implementation_mode,run.summary?.implementation_mode,run.state?.implementation_mode,run.launch?.implementation_mode,detail.summary?.implementation_mode,detail.state?.implementation_mode,detail.launch?.implementation_mode)}
|
| 25 |
function runRelativeTime(run={}){
|
| 26 |
const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';
|
|
@@ -72,7 +82,7 @@ function renderRunRows(runs,compact=false){
|
|
| 72 |
<p class="run-list-id" title="${escapeHtml(r.run_id)}">${escapeHtml(r.run_id)}</p>
|
| 73 |
</div>
|
| 74 |
<div class="run-list-status">
|
| 75 |
-
<em class="run-status-pill ${tone}" title="${escapeHtml(badgeLabel(status))}">${escapeHtml(
|
| 76 |
<time>${escapeHtml(time)}</time>
|
| 77 |
</div>
|
| 78 |
<button class="icon-btn danger run-delete-btn" type="button" data-run-delete="${escapeHtml(r.run_id)}" title="Delete this run" aria-label="Delete run ${escapeHtml(r.run_id)}">×</button>
|
|
@@ -264,103 +274,53 @@ function cachedRunDetail(runId,heavy=false){
|
|
| 264 |
if(heavy&&!entry.heavy)return null;
|
| 265 |
return entry.detail||null;
|
| 266 |
}
|
| 267 |
-
function mergeRunDetail(previous={},detail={}){
|
| 268 |
-
if(!previous||!Object.keys(previous).length)return detail;
|
| 269 |
-
const merged={...previous,...detail};
|
| 270 |
-
for(const key of ['summary','state','view','links','generation_smoke','inference_gate','repair_decision','blockage']){
|
| 271 |
-
if(previous[key]&&detail[key])merged[key]={...previous[key],...detail[key]};
|
| 272 |
-
}
|
| 273 |
-
for(const key of ['events','files','run_documents','timeline']){
|
| 274 |
-
if(Array.isArray(detail[key]))merged[key]=detail[key];
|
| 275 |
-
else if(Array.isArray(previous[key]))merged[key]=previous[key];
|
| 276 |
-
}
|
| 277 |
-
return merged;
|
| 278 |
-
}
|
| 279 |
function rememberRunDetail(runId,detail,heavy=false){
|
| 280 |
if(!runId||!detail)return;
|
| 281 |
state.runDetailCache=state.runDetailCache||{};
|
| 282 |
const previous=state.runDetailCache[runId]||{};
|
| 283 |
-
|
| 284 |
-
state.runDetailCache[runId]={detail:merged,heavy:Boolean(heavy||previous.heavy),ts:Date.now()};
|
| 285 |
-
}
|
| 286 |
-
function runDetailStatus(detail={}){
|
| 287 |
-
return String(detail.view?.header?.status||detail.status||detail.state?.status||detail.summary?.status||detail.generation_smoke?.status||'').toLowerCase();
|
| 288 |
-
}
|
| 289 |
-
function isTerminalRunDetail(detail={}){
|
| 290 |
-
const s=runDetailStatus(detail);
|
| 291 |
-
return Boolean(s)&&TERMINAL_STATUSES.has(s);
|
| 292 |
-
}
|
| 293 |
-
function stopActiveRunPolling(){
|
| 294 |
-
if(state.poll){clearInterval(state.poll);state.poll=null}
|
| 295 |
-
}
|
| 296 |
-
function renderCachedBuildRun(runId,detail={}){
|
| 297 |
-
switchCenterTab('active');
|
| 298 |
-
setBuildModeInspecting(runId);
|
| 299 |
-
state.runId=runId;
|
| 300 |
-
if(detail.bucket_source)state.bucketSource=detail.bucket_source;
|
| 301 |
-
renderRunDetail(detail);
|
| 302 |
-
if(detail.view||Array.isArray(detail.timeline)||Array.isArray(detail.events)||typeof detail.progress!=='undefined'){
|
| 303 |
-
renderProgress({...detail,run_id:detail.run_id||detail.summary?.run_id||runId});
|
| 304 |
-
}
|
| 305 |
-
saveActiveRun();
|
| 306 |
-
}
|
| 307 |
-
function hydrateRunDetailInBackground(runId,{heavy=true,validation=false}={}){
|
| 308 |
-
if(!runId)return;
|
| 309 |
-
const includeHeavy=heavy?'true':'false';
|
| 310 |
-
apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}&include_heavy=${includeHeavy}`).then(full=>{
|
| 311 |
-
rememberRunDetail(runId,full,heavy);
|
| 312 |
-
if(full.bucket_source)state.bucketSource=full.bucket_source;
|
| 313 |
-
if(validation){
|
| 314 |
-
if((state.validationRunId||'')===runId)showValidationRunDetail(full);
|
| 315 |
-
}else if(state.runId===runId){
|
| 316 |
-
renderCachedBuildRun(runId,cachedRunDetail(runId,heavy)||full);
|
| 317 |
-
}
|
| 318 |
-
}).catch(e=>console.warn(`${heavy?'heavy':'light'} run hydration failed`,e));
|
| 319 |
}
|
| 320 |
async function selectRun(runId){
|
| 321 |
// Keep the user on the current tab while refreshing the selected run panel.
|
| 322 |
-
// Keep selection snappy: render cached/optimistic data immediately, then hydrate once.
|
| 323 |
-
// Hydration uses include_heavy=false first and include_heavy=true in hydrateRunDetailInBackground().
|
| 324 |
-
// Background heavy run hydration failed messages are logged by the shared hydrator.
|
| 325 |
if(!runId)return;
|
| 326 |
document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===runId));
|
| 327 |
const cachedHeavy=cachedRunDetail(runId,true);
|
| 328 |
const cachedLight=cachedRunDetail(runId,false);
|
| 329 |
-
|
| 330 |
-
|
| 331 |
if(isValidationRunDetail(cached)){
|
| 332 |
showValidationRunDetail(cached);
|
| 333 |
-
if(!cachedHeavy)hydrateRunDetailInBackground(runId,{heavy:true,validation:true});
|
| 334 |
-
return;
|
| 335 |
-
}
|
| 336 |
-
renderCachedBuildRun(runId,cached);
|
| 337 |
-
if(isTerminalRunDetail(cached)){
|
| 338 |
-
stopActiveRunPolling();
|
| 339 |
-
if(!cachedHeavy)hydrateRunDetailInBackground(runId,{heavy:true,validation:false});
|
| 340 |
}else{
|
|
|
|
|
|
|
| 341 |
startPolling(runId);
|
|
|
|
| 342 |
}
|
| 343 |
-
|
|
|
|
| 344 |
}
|
| 345 |
-
renderRunSelectionPreview(runId);
|
| 346 |
try{
|
| 347 |
-
const detail=await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}&include_heavy=false`);
|
| 348 |
rememberRunDetail(runId,detail,false);
|
| 349 |
if(detail.bucket_source)state.bucketSource=detail.bucket_source;
|
| 350 |
if(isValidationRunDetail(detail)){
|
| 351 |
showValidationRunDetail(detail);
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
}
|
| 363 |
-
hydrateRunDetailInBackground(runId,{heavy:true,validation:false});
|
| 364 |
}catch(e){
|
| 365 |
showMessage(`Could not load run details: ${e.message}`,'error');
|
| 366 |
const report=$('reportPreview');if(report)report.textContent='Could not load run details: '+e.message;
|
|
@@ -475,9 +435,6 @@ function renderRunDocuments(detail={}){
|
|
| 475 |
const root=$('runDocumentsDock');
|
| 476 |
if(!root)return;
|
| 477 |
const docs=deriveRunDocuments(detail);
|
| 478 |
-
const signature=JSON.stringify(docs.map(d=>[d.id,d.present,d.url,d.label,d.subtitle,d.tone,d.sensitivity]));
|
| 479 |
-
if(root.dataset.signature===signature)return;
|
| 480 |
-
root.dataset.signature=signature;
|
| 481 |
if(!docs.length){root.innerHTML='<div class="empty">Run documents will appear as files are written to the bucket.</div>';return;}
|
| 482 |
root.innerHTML=docs.map(doc=>{
|
| 483 |
const present=Boolean(doc.present&&doc.url);
|
|
@@ -485,7 +442,7 @@ function renderRunDocuments(detail={}){
|
|
| 485 |
const attrs=present?`href="${escapeHtml(doc.url)}" target="_blank" rel="noreferrer"`:'';
|
| 486 |
const title=present?`Open ${doc.label}`:`${doc.label} is not in the bucket yet`;
|
| 487 |
const cls=['run-doc-btn',present?'ready':'pending',doc.sensitivity==='raw'?'raw':'',doc.sensitivity==='safe'?'safe':'',doc.tone==='warn'?'warn':''].filter(Boolean).join(' ');
|
| 488 |
-
return `<${tag} class="${cls}" ${attrs} title="${escapeHtml(title)}" aria-disabled="${present?'false':'true'}"><span class="doc-icon">${escapeHtml(doc.icon||'
|
| 489 |
}).join('');
|
| 490 |
}
|
| 491 |
|
|
@@ -550,20 +507,16 @@ function renderAgentRecovery(detail={}){
|
|
| 550 |
const diagnosisEvent=latestRecoveryEvent(detail,['failure_diagnosis','pi_diagnosis','repair_decision']);
|
| 551 |
const rebuildEvent=latestRecoveryEvent(detail,['wait_for_logs','factory_rebuild']);
|
| 552 |
const patchEvent=latestRecoveryEvent(detail,['repair_brief','repair_plan','repair_patch','repair_upload','repair_validation']);
|
| 553 |
-
const validationEvent=latestRecoveryEvent(detail,['repair_validation','failed_after_repair','technical_blocker','manual_hardware_required']);
|
| 554 |
const reason=decision.reason||blockage.reason||blockage.message||diagnosisEvent?.message||'Pi diagnosis is coordinating the next recovery action from collected logs, status and validation evidence.';
|
| 555 |
const evidence=Array.isArray(decision.evidence)?decision.evidence:[];
|
| 556 |
const next=decision.next_step||blockage.suggested_next_step||blockage.next_step||'';
|
| 557 |
-
const signature=JSON.stringify({action,normalizedAction,tone,status,reason,evidence:evidence.slice(0,3),next,decision,blockage,diagnosis:diagnosisEvent?.ts,rebuild:rebuildEvent?.ts,patch:patchEvent?.ts,validation:validationEvent?.ts});
|
| 558 |
-
if(body.dataset.signature===signature)return;
|
| 559 |
-
body.dataset.signature=signature;
|
| 560 |
const rows=[];
|
| 561 |
rows.push(`<div class="recovery-summary"><strong>${escapeHtml(normalizedAction)}</strong><p>${escapeHtml(shortText(reason,220))}</p></div>`);
|
| 562 |
rows.push(`<div class="recovery-ladder" aria-label="Agent recovery phases">
|
| 563 |
<span class="${diagnosisEvent||decision.action?'done':'pending'}"><b>1</b><em>Diagnose</em></span>
|
| 564 |
<span class="${decision.action?'done':'pending'}"><b>2</b><em>Decide</em></span>
|
| 565 |
<span class="${rebuildEvent?'done':patchEvent?'done':decision.action?'running':'pending'}"><b>3</b><em>${escapeHtml(action&&String(action).includes('rebuild')?'Rebuild':action&&String(action).includes('patch')?'Patch':'Act')}</em></span>
|
| 566 |
-
<span class="${
|
| 567 |
</div>`);
|
| 568 |
const meta=[];
|
| 569 |
if(decision.failure_category||blockage.failure_category)meta.push(['Failure',decision.failure_category||blockage.failure_category]);
|
|
@@ -630,4 +583,4 @@ function renderRunDetail(detail){
|
|
| 630 |
switchCenterTab('active');
|
| 631 |
}
|
| 632 |
|
| 633 |
-
// Build run routing invariant: switchCenterTab('active');setBuildModeInspecting(runId);startPolling(runId)
|
|
|
|
| 20 |
}
|
| 21 |
function filteredRuns(){return (state.runsCache||[]).filter(r=>runMatchesStatus(r,state.runStatusFilter||'all')&&runMatchesQuery(r,state.runSearch||''))}
|
| 22 |
function renderRunStats(){const counts={all:state.runsCache.length,running:0,full_inference_success:0,manual_hardware_required:0,failed:0,stale:0};for(const r of state.runsCache){if(runMatchesStatus(r,'running'))counts.running+=1;if(runMatchesStatus(r,'full_inference_success'))counts.full_inference_success+=1;if(runMatchesStatus(r,'manual_hardware_required'))counts.manual_hardware_required+=1;if(runMatchesStatus(r,'failed'))counts.failed+=1;if(runMatchesStatus(r,'stale'))counts.stale+=1}document.querySelectorAll('.filter[data-status]').forEach(btn=>{const status=btn.dataset.status||'all';const label={all:'All',running:'Running',full_inference_success:'Success',manual_hardware_required:'Manual',failed:'Failed',technical_blocker:'Blockers',stale:'Stale'}[status]||status;const count=counts[status];btn.innerHTML=typeof count==='number'?`${escapeHtml(label)} <span>${count}</span>`:escapeHtml(label);btn.classList.toggle('active',status===state.runStatusFilter)})}
|
| 23 |
+
function runCardKind(run={}){return isValidationRunSummary(run)?'Test':'Build'}
|
| 24 |
+
function runCompactStatusLabel(status){
|
| 25 |
+
const s=String(status||'unknown').toLowerCase();
|
| 26 |
+
if(s.includes('success')||s.includes('succeed')||s.includes('passed')||s==='done'||s==='completed'||s==='health_only')return 'Success';
|
| 27 |
+
if(s.includes('running')||s.includes('queued')||s.includes('pending')||s.includes('building')||s.includes('waiting'))return 'Running';
|
| 28 |
+
if(s.includes('manual')||s.includes('hardware'))return 'Manual';
|
| 29 |
+
if(s.includes('stale'))return 'Stale';
|
| 30 |
+
if(s.includes('cancelled')||s.includes('canceled')||s.includes('stopped'))return 'Stopped';
|
| 31 |
+
if(s.includes('failed')||s.includes('error')||s.includes('technical')||s.includes('blocker'))return 'Failed';
|
| 32 |
+
return badgeLabel(status);
|
| 33 |
+
}
|
| 34 |
function runImplementationMode(run={},detail={}){return firstNonEmpty(run.implementation_mode,run.summary?.implementation_mode,run.state?.implementation_mode,run.launch?.implementation_mode,detail.summary?.implementation_mode,detail.state?.implementation_mode,detail.launch?.implementation_mode)}
|
| 35 |
function runRelativeTime(run={}){
|
| 36 |
const raw=run.updated_at||run.created_at||run.started_at||run.cancelled_at||'';
|
|
|
|
| 82 |
<p class="run-list-id" title="${escapeHtml(r.run_id)}">${escapeHtml(r.run_id)}</p>
|
| 83 |
</div>
|
| 84 |
<div class="run-list-status">
|
| 85 |
+
<em class="run-status-pill ${tone}" title="${escapeHtml(badgeLabel(status))}">${escapeHtml(runCompactStatusLabel(status))}</em>
|
| 86 |
<time>${escapeHtml(time)}</time>
|
| 87 |
</div>
|
| 88 |
<button class="icon-btn danger run-delete-btn" type="button" data-run-delete="${escapeHtml(r.run_id)}" title="Delete this run" aria-label="Delete run ${escapeHtml(r.run_id)}">×</button>
|
|
|
|
| 274 |
if(heavy&&!entry.heavy)return null;
|
| 275 |
return entry.detail||null;
|
| 276 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
function rememberRunDetail(runId,detail,heavy=false){
|
| 278 |
if(!runId||!detail)return;
|
| 279 |
state.runDetailCache=state.runDetailCache||{};
|
| 280 |
const previous=state.runDetailCache[runId]||{};
|
| 281 |
+
state.runDetailCache[runId]={detail,heavy:Boolean(heavy||previous.heavy),ts:Date.now()};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
}
|
| 283 |
async function selectRun(runId){
|
| 284 |
// Keep the user on the current tab while refreshing the selected run panel.
|
|
|
|
|
|
|
|
|
|
| 285 |
if(!runId)return;
|
| 286 |
document.querySelectorAll('.run-row').forEach(row=>row.classList.toggle('selected',row.dataset.run===runId));
|
| 287 |
const cachedHeavy=cachedRunDetail(runId,true);
|
| 288 |
const cachedLight=cachedRunDetail(runId,false);
|
| 289 |
+
if(cachedHeavy||cachedLight){
|
| 290 |
+
const cached=cachedHeavy||cachedLight;
|
| 291 |
if(isValidationRunDetail(cached)){
|
| 292 |
showValidationRunDetail(cached);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
}else{
|
| 294 |
+
switchCenterTab('active');
|
| 295 |
+
setBuildModeInspecting(runId);
|
| 296 |
startPolling(runId);
|
| 297 |
+
renderRunDetail(cached);
|
| 298 |
}
|
| 299 |
+
}else{
|
| 300 |
+
renderRunSelectionPreview(runId);
|
| 301 |
}
|
|
|
|
| 302 |
try{
|
| 303 |
+
const detail=cachedLight||await apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}&include_heavy=false`);
|
| 304 |
rememberRunDetail(runId,detail,false);
|
| 305 |
if(detail.bucket_source)state.bucketSource=detail.bucket_source;
|
| 306 |
if(isValidationRunDetail(detail)){
|
| 307 |
showValidationRunDetail(detail);
|
| 308 |
+
if(!cachedHeavy){
|
| 309 |
+
apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}&include_heavy=true`).then(full=>{
|
| 310 |
+
rememberRunDetail(runId,full,true);
|
| 311 |
+
if((state.validationRunId||'')===runId){if(full.bucket_source)state.bucketSource=full.bucket_source;showValidationRunDetail(full)}
|
| 312 |
+
}).catch(e=>console.warn('heavy validation hydration failed',e));
|
| 313 |
+
}
|
| 314 |
+
return}
|
| 315 |
+
switchCenterTab('active');
|
| 316 |
+
setBuildModeInspecting(runId);
|
| 317 |
+
startPolling(runId);renderRunDetail(detail);
|
| 318 |
+
if(!cachedHeavy){
|
| 319 |
+
apiGet(`/api/runs/${encodeURIComponent(runId)}?bucket_name=${encodeURIComponent(state.bucketName)}&include_heavy=true`).then(full=>{
|
| 320 |
+
rememberRunDetail(runId,full,true);
|
| 321 |
+
if(state.runId===runId){if(full.bucket_source)state.bucketSource=full.bucket_source;renderRunDetail(full)}
|
| 322 |
+
}).catch(e=>console.warn('heavy run hydration failed',e));
|
| 323 |
}
|
|
|
|
| 324 |
}catch(e){
|
| 325 |
showMessage(`Could not load run details: ${e.message}`,'error');
|
| 326 |
const report=$('reportPreview');if(report)report.textContent='Could not load run details: '+e.message;
|
|
|
|
| 435 |
const root=$('runDocumentsDock');
|
| 436 |
if(!root)return;
|
| 437 |
const docs=deriveRunDocuments(detail);
|
|
|
|
|
|
|
|
|
|
| 438 |
if(!docs.length){root.innerHTML='<div class="empty">Run documents will appear as files are written to the bucket.</div>';return;}
|
| 439 |
root.innerHTML=docs.map(doc=>{
|
| 440 |
const present=Boolean(doc.present&&doc.url);
|
|
|
|
| 442 |
const attrs=present?`href="${escapeHtml(doc.url)}" target="_blank" rel="noreferrer"`:'';
|
| 443 |
const title=present?`Open ${doc.label}`:`${doc.label} is not in the bucket yet`;
|
| 444 |
const cls=['run-doc-btn',present?'ready':'pending',doc.sensitivity==='raw'?'raw':'',doc.sensitivity==='safe'?'safe':'',doc.tone==='warn'?'warn':''].filter(Boolean).join(' ');
|
| 445 |
+
return `<${tag} class="${cls}" ${attrs} title="${escapeHtml(title)}" aria-disabled="${present?'false':'true'}"><span class="doc-icon">${escapeHtml(doc.icon||'📄')}</span><span><strong>${escapeHtml(doc.label)}</strong><em>${escapeHtml(present?doc.subtitle||'Open document':'Pending')}</em></span></${tag}>`;
|
| 446 |
}).join('');
|
| 447 |
}
|
| 448 |
|
|
|
|
| 507 |
const diagnosisEvent=latestRecoveryEvent(detail,['failure_diagnosis','pi_diagnosis','repair_decision']);
|
| 508 |
const rebuildEvent=latestRecoveryEvent(detail,['wait_for_logs','factory_rebuild']);
|
| 509 |
const patchEvent=latestRecoveryEvent(detail,['repair_brief','repair_plan','repair_patch','repair_upload','repair_validation']);
|
|
|
|
| 510 |
const reason=decision.reason||blockage.reason||blockage.message||diagnosisEvent?.message||'Pi diagnosis is coordinating the next recovery action from collected logs, status and validation evidence.';
|
| 511 |
const evidence=Array.isArray(decision.evidence)?decision.evidence:[];
|
| 512 |
const next=decision.next_step||blockage.suggested_next_step||blockage.next_step||'';
|
|
|
|
|
|
|
|
|
|
| 513 |
const rows=[];
|
| 514 |
rows.push(`<div class="recovery-summary"><strong>${escapeHtml(normalizedAction)}</strong><p>${escapeHtml(shortText(reason,220))}</p></div>`);
|
| 515 |
rows.push(`<div class="recovery-ladder" aria-label="Agent recovery phases">
|
| 516 |
<span class="${diagnosisEvent||decision.action?'done':'pending'}"><b>1</b><em>Diagnose</em></span>
|
| 517 |
<span class="${decision.action?'done':'pending'}"><b>2</b><em>Decide</em></span>
|
| 518 |
<span class="${rebuildEvent?'done':patchEvent?'done':decision.action?'running':'pending'}"><b>3</b><em>${escapeHtml(action&&String(action).includes('rebuild')?'Rebuild':action&&String(action).includes('patch')?'Patch':'Act')}</em></span>
|
| 519 |
+
<span class="${latestRecoveryEvent(detail,['repair_validation','generation_smoke','inference_gate','failure'])?'done':'pending'}"><b>4</b><em>Revalidate</em></span>
|
| 520 |
</div>`);
|
| 521 |
const meta=[];
|
| 522 |
if(decision.failure_category||blockage.failure_category)meta.push(['Failure',decision.failure_category||blockage.failure_category]);
|
|
|
|
| 583 |
switchCenterTab('active');
|
| 584 |
}
|
| 585 |
|
| 586 |
+
// Build run routing invariant: switchCenterTab('active');setBuildModeInspecting(runId);startPolling(runId)
|