fffiloni commited on
Commit
c1f7b6c
·
verified ·
1 Parent(s): db878b5

Upload 6 files

Browse files
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)}&include_heavy=true`);
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;if(typeof rememberRunDetail==='function')rememberRunDetail(state.runId,{...p,run_id:p.run_id||state.runId},false);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={}){
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=opts.instant?'auto':'smooth';
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)?'Space Test':'Build'}
 
 
 
 
 
 
 
 
 
 
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(badgeLabel(status))}</em>
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
- const merged=mergeRunDetail(previous.detail||{},detail);
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
- const cached=cachedHeavy||cachedLight;
330
- if(cached){
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
- return;
 
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
- hydrateRunDetailInBackground(runId,{heavy:true,validation:true});
353
- return;
354
- }
355
- renderCachedBuildRun(runId,detail);
356
- if(isTerminalRunDetail(detail)){
357
- stopActiveRunPolling();
358
- // One progress refresh updates timeline/events without starting a polling interval.
359
- refreshProgress();
360
- }else{
361
- startPolling(runId);
 
 
 
 
 
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||'DOC')}</span><span><strong>${escapeHtml(doc.label)}</strong><em>${escapeHtml(present?doc.subtitle||'Open document':'Pending')}</em></span></${tag}>`;
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="${validationEvent?'done':(rebuildEvent||patchEvent)?'running':'pending'}"><b>4</b><em>Revalidate</em></span>
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);renderRunDetail(detail)
 
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)