fffiloni commited on
Commit
d1592ef
Β·
verified Β·
1 Parent(s): a50bfae

Upload 7 files

Browse files
web/static/app.css CHANGED
@@ -2366,3 +2366,91 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
2366
  .eval-archive-meta strong{display:block;margin-top:2px;font-size:12px;color:#101828;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2367
  .eval-archive-path{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;background:#f8fafc;border:1px solid #eef2f7;border-radius:10px;padding:7px 8px;overflow:auto}
2368
  @media (max-width:520px){.eval-archive-meta{grid-template-columns:1fr}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2366
  .eval-archive-meta strong{display:block;margin-top:2px;font-size:12px;color:#101828;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2367
  .eval-archive-path{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;background:#f8fafc;border:1px solid #eef2f7;border-radius:10px;padding:7px 8px;overflow:auto}
2368
  @media (max-width:520px){.eval-archive-meta{grid-template-columns:1fr}}
2369
+
2370
+ /* v189.2: canonical run timeline bridge */
2371
+ .timeline.timeline-canonical .step small{
2372
+ display:block;
2373
+ max-width:180px;
2374
+ margin:0 auto;
2375
+ }
2376
+ .timeline.timeline-canonical .canonical-summary{
2377
+ color:var(--muted);
2378
+ }
2379
+ .timeline.timeline-canonical .step.status-warn .canonical-summary{
2380
+ color:#92400e;
2381
+ }
2382
+ .timeline.timeline-canonical .step.status-skipped{
2383
+ opacity:.72;
2384
+ }
2385
+
2386
+ /* v189.3 β€” semantic progress: process completion is separate from result verdict */
2387
+ .progress-bar>div.is-warn{background:linear-gradient(90deg,#d97706,#fbbf24)!important}
2388
+ .progress-semantics{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;margin:8px 0 0;font-size:11.5px;color:#475467;line-height:1.35}
2389
+ .progress-semantics[hidden]{display:none!important}
2390
+ .progress-semantics span{display:inline-flex;gap:4px;align-items:center;border:1px solid #e5e7eb;background:#fff;border-radius:999px;padding:4px 8px;min-width:0}
2391
+ .progress-semantics strong{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:#667085}
2392
+ .progress-semantics em{font-style:normal;color:#667085;flex-basis:100%;min-width:0}
2393
+ .progress-semantics.success span{border-color:#bbf7d0;background:#ecfdf3;color:#166534}
2394
+ .progress-semantics.warn span{border-color:#fed7aa;background:#fff7ed;color:#9a3412}
2395
+ .progress-semantics.error span{border-color:#fecdd3;background:#fff1f2;color:#be123c}
2396
+ .progress-semantics.stopped span{border-color:#cbd5e1;background:#f8fafc;color:#475569}
2397
+ .progress-semantics.running span{border-color:#bfdbfe;background:#eff6ff;color:#1d4ed8}
2398
+
2399
+ /* v189.4 β€” mobile-first canonical timeline details and compact readable phases */
2400
+ .timeline-phase-details[hidden]{display:none!important}
2401
+ .timeline-phase-details{
2402
+ margin:10px 0 0;
2403
+ border:1px solid #e5e7eb;
2404
+ background:#fff;
2405
+ border-radius:14px;
2406
+ padding:10px 12px;
2407
+ box-shadow:0 8px 20px rgba(16,24,40,.04);
2408
+ }
2409
+ .timeline-detail-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:6px}
2410
+ .timeline-detail-head span{font-weight:700;color:#101828;font-size:13px}
2411
+ .timeline-detail-head strong{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#667085;background:#f8fafc;border:1px solid #e5e7eb;border-radius:999px;padding:4px 8px;white-space:nowrap}
2412
+ .timeline-phase-details>p{margin:0 0 8px;color:#475467;font-size:12px;line-height:1.4}
2413
+ .timeline-phase-details ul{display:grid;gap:6px;list-style:none;margin:0;padding:0}
2414
+ .timeline-detail-row{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid #eef2f7;background:#f8fafc;border-radius:10px;padding:7px 8px;font-size:12px;min-width:0}
2415
+ .timeline-detail-row span{color:#667085;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2416
+ .timeline-detail-row strong{color:#101828;font-weight:650;text-align:right;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2417
+ .timeline-detail-warnings{margin-top:8px;border:1px solid #fed7aa;background:#fff7ed;border-radius:10px;padding:8px 9px;color:#9a3412}
2418
+ .timeline-detail-warnings strong{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px}
2419
+ .timeline-detail-warnings p{margin:2px 0 0;font-size:12px;line-height:1.35}
2420
+ .timeline-phase-details.done,.timeline-phase-details.success{border-color:#bbf7d0;background:#f0fdf4}
2421
+ .timeline-phase-details.warn,.timeline-phase-details.warning,.timeline-phase-details.fallback{border-color:#fed7aa;background:#fff7ed}
2422
+ .timeline-phase-details.failed,.timeline-phase-details.error{border-color:#fecdd3;background:#fff1f2}
2423
+ .timeline-phase-details.skipped,.timeline-phase-details.muted{background:#f8fafc;opacity:.92}
2424
+ @media (max-width:720px){
2425
+ .timeline.timeline-canonical{
2426
+ display:grid!important;
2427
+ grid-template-columns:1fr!important;
2428
+ gap:8px!important;
2429
+ overflow:visible!important;
2430
+ padding:4px 0!important;
2431
+ }
2432
+ .timeline.timeline-canonical .step{
2433
+ min-width:0!important;
2434
+ display:grid!important;
2435
+ grid-template-columns:34px 1fr!important;
2436
+ text-align:left!important;
2437
+ align-items:start!important;
2438
+ gap:9px!important;
2439
+ padding:8px 10px!important;
2440
+ border:1px solid #eef2f7!important;
2441
+ border-radius:12px!important;
2442
+ background:#fff!important;
2443
+ }
2444
+ .timeline.timeline-canonical .step:before,
2445
+ .timeline.timeline-canonical .step:after{display:none!important}
2446
+ .timeline.timeline-canonical .step-dot{margin:0!important;width:28px!important;height:28px!important}
2447
+ .timeline.timeline-canonical .step strong,
2448
+ .timeline.timeline-canonical .step small,
2449
+ .timeline.timeline-canonical .step .canonical-summary{max-width:none!important;text-align:left!important;margin:0!important}
2450
+ .timeline.timeline-canonical .step small{font-size:11.5px!important;line-height:1.25!important}
2451
+ .timeline-phase-details{margin-top:8px;padding:9px 10px;border-radius:12px}
2452
+ .timeline-detail-row{align-items:flex-start;flex-direction:column;gap:3px}
2453
+ .timeline-detail-row strong{text-align:left;white-space:normal}
2454
+ .progress-semantics{gap:6px;font-size:11px}
2455
+ .progress-semantics span{width:100%;justify-content:space-between}
2456
+ }
web/static/app.js CHANGED
@@ -223,7 +223,7 @@ function setBucketFeedback(mode='checking',message='Checking private bucket acce
223
  }
224
  }
225
  function renderAnonymousEvalStatus(me){
226
- const card=$('runEvalArchiveCard');
227
  const title=$('evalArchiveTitle');
228
  const text=$('anonymousEvalText');
229
  const enable=$('enableEvalArchive');
@@ -245,8 +245,9 @@ function renderAnonymousEvalStatus(me){
245
  title.textContent=enabled?'Anonymous eval archive':'Anonymous eval archive';
246
  }
247
  if(stateBadge){
248
- stateBadge.textContent=enabled?'On':'Off';
249
- stateBadge.className='badge '+(enabled?'success':'neutral');
 
250
  }
251
  if(text){
252
  text.className='eval-feedback '+(enabled?'on':'off');
@@ -254,6 +255,9 @@ function renderAnonymousEvalStatus(me){
254
  const manager=cfg.managed_by==='you'?'You manage this instance setting.':`Managed by ${cfg.managed_by||'instance owner'}.`;
255
  text.textContent=`On Β· ${bucket}${path} β†’ ${mount}`;
256
  text.title=`Anonymous project eval archive enabled from ${cfg.source||'config'}: ${bucket}${path} mounted at ${mount}. ${manager} ASF backend publishes anonymized eval records after user Jobs write local run evals.`;
 
 
 
257
  }else if(me?.username){
258
  text.textContent='Mount an operator eval bucket at /evals, then enable publishing.';
259
  text.title='Recommended: private operator bucket, bucket path evals, mounted read-write at /evals on this ASF Space. Env vars still work as an admin override.';
@@ -263,7 +267,7 @@ function renderAnonymousEvalStatus(me){
263
  }
264
  }
265
  if(enable){
266
- enable.hidden=enabled||!me?.username||cfg.env_override===true;
267
  enable.disabled=!me?.username||state.busy;
268
  enable.title=`Enable anonymous eval archive using ${cfg.proposed_bucket_source||'your namespace bucket'} mounted at ${cfg.default_mount_path||'/evals'}.`;
269
  }
@@ -391,7 +395,7 @@ function resetRunDetails(){
391
  ])setText(id,value);
392
  setText('activeLatencyValue','β€”');setText('activeZeroGpuDuration','β€”');setText('activeLatencySource','waiting for live smoke test');const latencyCard=$('activeLatencyCard');if(latencyCard){latencyCard.classList.remove('has-latency');latencyCard.classList.add('is-loading')}
393
  const status=$('progressStatus');if(status)status.className='badge neutral';
394
- const fill=$('progressFill');if(fill){fill.style.width='0%';fill.classList.remove('is-running','progress-bump')}
395
  setQuickLinks({});
396
  const full=$('reportPreview');if(full)full.textContent='No report selected.';
397
  const manual=$('manualActionPanel');if(manual)manual.hidden=true;
@@ -405,7 +409,7 @@ function resetRunDetails(){
405
  const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''}
406
  for(const id of ['deleteActiveRun','activePrefillSpaceTest','deleteValidationRun']){const btn=$(id);if(btn)btn.disabled=true}
407
  renderTimeline(DEFAULT_TIMELINE,'idle');
408
- } function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.activeRunProgress=null;state.selectedRunDetail=null;state.documentCache={};state.latencyCache={};state.activeLinks={};state.pollErrors=0;clearSavedRun();document.querySelectorAll('.run-row').forEach(r=>r.classList.remove('selected'));setBuildModeNew();resetRunDetails();renderTimeline(DEFAULT_TIMELINE);setText('progressPercent','0%');const fill=$('progressFill');if(fill)fill.style.width='0%';setText('currentStep','Ready');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','β€”');if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;setText('lastPolled','β€”');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
409
  function setActiveRun(result){if(state.poll){clearInterval(state.poll);state.poll=null}resetRunDetails();state.documentCache={};state.latencyCache={};state.runId=result.run_id;state.spaceLinksUnlockedFor=null;if(Array.isArray(state.runsCache)&&typeof sortRunsNewestFirst==='function'){state.runsCache=sortRunsNewestFirst([{...result,run_id:result.run_id,status:result.status||'running',created_at:result.created_at||new Date().toISOString()},...state.runsCache.filter(r=>r.run_id!==result.run_id)]);renderRunsFromCache();}if(result.bucket_source)state.bucketSource=result.bucket_source;setText('detailRunId',result.run_id);setText('detailJobStatus',result.status||'RUNNING');setText('detailGateStatus',result.kind||'running');setText('detailSpace',result.target_space||'β€”');setText('detailHardware',result.selected_hardware||result.preferred_space_hardware||'β€”');renderActiveRunMeta({summary:result,state:result,launch:result});renderProgress({run_id:result.run_id,status:result.status||'running',progress:1,current_step_label:'Launching build Job…',events:[{step:'launch',status:'started',message:'Build Job launched; waiting for first worker event.'}],summary:result,state:result});setText('homeRunDetailStatus',result.status||'Running');setBuildModeInspecting(result.run_id);setQuickLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username,target_space_url:result.target_space_url},result);saveActiveRun();markPendingActiveFields();startPolling(result.run_id)}
410
  function startPolling(runId,options={}){
411
  if(!runId)return;
 
223
  }
224
  }
225
  function renderAnonymousEvalStatus(me){
226
+ const card=$('evalArchiveCard');
227
  const title=$('evalArchiveTitle');
228
  const text=$('anonymousEvalText');
229
  const enable=$('enableEvalArchive');
 
245
  title.textContent=enabled?'Anonymous eval archive':'Anonymous eval archive';
246
  }
247
  if(stateBadge){
248
+ const unavailable=String(cfg.reason||'')==='status_unavailable';
249
+ stateBadge.textContent=enabled?'On':(unavailable?'Unknown':'Off');
250
+ stateBadge.className='badge '+(enabled?'success':unavailable?'warn':'neutral');
251
  }
252
  if(text){
253
  text.className='eval-feedback '+(enabled?'on':'off');
 
255
  const manager=cfg.managed_by==='you'?'You manage this instance setting.':`Managed by ${cfg.managed_by||'instance owner'}.`;
256
  text.textContent=`On Β· ${bucket}${path} β†’ ${mount}`;
257
  text.title=`Anonymous project eval archive enabled from ${cfg.source||'config'}: ${bucket}${path} mounted at ${mount}. ${manager} ASF backend publishes anonymized eval records after user Jobs write local run evals.`;
258
+ }else if(String(cfg.reason||'')==='status_unavailable'){
259
+ text.textContent='Status unavailable Β· could not verify eval archive configuration.';
260
+ text.title='Refresh to retry the public eval archive status check. Unknown is not treated as Off.';
261
  }else if(me?.username){
262
  text.textContent='Mount an operator eval bucket at /evals, then enable publishing.';
263
  text.title='Recommended: private operator bucket, bucket path evals, mounted read-write at /evals on this ASF Space. Env vars still work as an admin override.';
 
267
  }
268
  }
269
  if(enable){
270
+ enable.hidden=enabled||!me?.username||cfg.env_override===true||cfg.can_manage===false;
271
  enable.disabled=!me?.username||state.busy;
272
  enable.title=`Enable anonymous eval archive using ${cfg.proposed_bucket_source||'your namespace bucket'} mounted at ${cfg.default_mount_path||'/evals'}.`;
273
  }
 
395
  ])setText(id,value);
396
  setText('activeLatencyValue','β€”');setText('activeZeroGpuDuration','β€”');setText('activeLatencySource','waiting for live smoke test');const latencyCard=$('activeLatencyCard');if(latencyCard){latencyCard.classList.remove('has-latency');latencyCard.classList.add('is-loading')}
397
  const status=$('progressStatus');if(status)status.className='badge neutral';
398
+ const fill=$('progressFill');if(fill){fill.style.width='0%';fill.classList.remove('is-running','progress-bump','is-success','is-warn','is-error','is-running-state','is-stopped','is-neutral')}const cap=$('progressCaptionLabel');if(cap)cap.textContent='Overall progress';const sem=$('progressSemantics');if(sem){sem.hidden=true;sem.innerHTML=''}const bar=$('progressBar');if(bar)bar.setAttribute('aria-valuenow','0')
399
  setQuickLinks({});
400
  const full=$('reportPreview');if(full)full.textContent='No report selected.';
401
  const manual=$('manualActionPanel');if(manual)manual.hidden=true;
 
409
  const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''}
410
  for(const id of ['deleteActiveRun','activePrefillSpaceTest','deleteValidationRun']){const btn=$(id);if(btn)btn.disabled=true}
411
  renderTimeline(DEFAULT_TIMELINE,'idle');
412
+ } function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.activeRunProgress=null;state.selectedRunDetail=null;state.documentCache={};state.latencyCache={};state.activeLinks={};state.pollErrors=0;clearSavedRun();document.querySelectorAll('.run-row').forEach(r=>r.classList.remove('selected'));setBuildModeNew();resetRunDetails();renderTimeline(DEFAULT_TIMELINE);setText('progressPercent','0%');const cap=$('progressCaptionLabel');if(cap)cap.textContent='Overall progress';const sem=$('progressSemantics');if(sem){sem.hidden=true;sem.innerHTML=''}const fill=$('progressFill');if(fill)fill.style.width='0%';setText('currentStep','Ready');setText('lastEvent','Choose a model card and launch a build.');setText('elapsed','β€”');if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;setText('lastPolled','β€”');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
413
  function setActiveRun(result){if(state.poll){clearInterval(state.poll);state.poll=null}resetRunDetails();state.documentCache={};state.latencyCache={};state.runId=result.run_id;state.spaceLinksUnlockedFor=null;if(Array.isArray(state.runsCache)&&typeof sortRunsNewestFirst==='function'){state.runsCache=sortRunsNewestFirst([{...result,run_id:result.run_id,status:result.status||'running',created_at:result.created_at||new Date().toISOString()},...state.runsCache.filter(r=>r.run_id!==result.run_id)]);renderRunsFromCache();}if(result.bucket_source)state.bucketSource=result.bucket_source;setText('detailRunId',result.run_id);setText('detailJobStatus',result.status||'RUNNING');setText('detailGateStatus',result.kind||'running');setText('detailSpace',result.target_space||'β€”');setText('detailHardware',result.selected_hardware||result.preferred_space_hardware||'β€”');renderActiveRunMeta({summary:result,state:result,launch:result});renderProgress({run_id:result.run_id,status:result.status||'running',progress:1,current_step_label:'Launching build Job…',events:[{step:'launch',status:'started',message:'Build Job launched; waiting for first worker event.'}],summary:result,state:result});setText('homeRunDetailStatus',result.status||'Running');setBuildModeInspecting(result.run_id);setQuickLinks({...result.links,bucket_source:result.bucket_source,run_id:result.run_id,job_id:result.job_id,job_owner:state.user?.username,target_space_url:result.target_space_url},result);saveActiveRun();markPendingActiveFields();startPolling(result.run_id)}
414
  function startPolling(runId,options={}){
415
  if(!runId)return;
web/static/progress.js CHANGED
@@ -270,6 +270,7 @@ function timelineIcon(status){
270
  if(status==='done'||status==='completed')return'βœ“';
271
  if(status==='running')return'β€’';
272
  if(status==='failed')return'!';
 
273
  if(status==='fallback')return'⇄';
274
  if(status==='recovered'||status==='superseded')return'β†Ί';
275
  if(status==='blocked'||status==='cancelled'||status==='stopped'||status==='after_fail')return'β– ';
@@ -282,6 +283,7 @@ function timelineStatus(status,overallStatus=''){
282
  const stopped=overall&&PROGRESS_TERMINAL_STATUSES.has(overall)&&!['success','succeeded','done','full_inference_success','full_inference_candidate_health_passed','health_only','partial','partial_validation','completed_with_warnings'].includes(overall);
283
  if(status==='failed'||status==='error'||status==='failure')return'status-failed';
284
  if(status==='fallback')return'status-fallback';
 
285
  if(status==='recovered'||status==='superseded')return'status-warn';
286
  if(status==='after_fail'||status==='post_failure')return'status-after-fail';
287
  if(status==='blocked'||status==='cancelled'||status==='canceled'||status==='stopped')return'status-stopped';
@@ -333,6 +335,9 @@ function friendlyTerminalEventMessage(event={}){
333
  const step=String(event.step||'').toLowerCase();
334
  const status=String(event.status||'').toLowerCase();
335
  const msg=String(event.message||'').trim();
 
 
 
336
  if(step==='done'&&(status==='full_inference_candidate_health_passed'||status==='health_only'||status==='partial_validation')){
337
  return 'Universal model-card builder completed Β· Partial validation';
338
  }
@@ -344,6 +349,163 @@ function friendlyTerminalEventMessage(event={}){
344
  function eventMessage(event){
345
  return friendlyTerminalEventMessage(event||{});
346
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  function timelineStepBounds(root,target){
348
  const rootBox=root.getBoundingClientRect?root.getBoundingClientRect():{left:0,right:root.clientWidth,width:root.clientWidth};
349
  const targetBox=target.getBoundingClientRect?target.getBoundingClientRect():{left:target.offsetLeft-root.scrollLeft,right:target.offsetLeft-root.scrollLeft+target.offsetWidth,width:target.offsetWidth};
@@ -522,6 +684,9 @@ function renderTimeline(items,overallStatus=''){
522
  const root=$('timeline');
523
  if(!root)return;
524
  if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{if(root.dataset.autoScrolling==='1')return;root.dataset.userScrolled='1'},{passive:true});}
 
 
 
525
  const fullList=items||[];
526
  const list=compactTimelineItems(fullList,overallStatus);
527
  // Keep the compact timeline on a single row. The number of visible groups is
@@ -712,7 +877,7 @@ function animateProgressBar(progress,status,currentStep){
712
  let klass=visualStatusClass(status);if(value>=100&&klass==='neutral')klass='success';
713
  fill.style.setProperty('--progress-target',`${value}%`);
714
  fill.style.width=`${value}%`;
715
- fill.classList.remove('is-success','is-error','is-running-state','is-stopped','is-neutral');
716
  fill.classList.add(`is-${klass==='running'?'running-state':klass}`);
717
  const s=String(status||'').toLowerCase();fill.classList.toggle('is-running',klass==='running'||s==='pending'||s==='scheduled'||s==='waiting');
718
  if(previous!==null&&Math.round(previous)!==Math.round(value)){
@@ -732,6 +897,40 @@ function animateStatusBadge(status){
732
  }
733
  progressMotionState.lastStatus=next;
734
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  function setElapsedClock(seconds,status){
736
  const s=String(status||'').toLowerCase();
737
  elapsedClock.baseSeconds=Math.max(0,Number(seconds||0));
@@ -776,7 +975,7 @@ function resetProgressStability(runId=null){
776
  progressMotionState.lastStatus=null;
777
  progressMotionState.lastStep=null;
778
  const timeline=$('timeline');
779
- if(timeline){timeline.dataset.signature='';timeline.dataset.visibleStep='';timeline.dataset.userScrolled='';timeline.classList.remove('trace-failed','trace-recovered','trace-fallback');timeline.scrollLeft=0;}const note=$('timelineTraceNote');if(note){note.hidden=true;note.innerHTML='';note.className='timeline-trace-note';}
780
  }
781
  function stabilizeProgressPayload(p={},view=null){
782
  const runId=String(p.run_id||view?.run_id||state.runId||'');
@@ -828,22 +1027,28 @@ function renderRunViewModel(view,p={}){
828
  const header=view.header||{};
829
  const sm=view.status_model||{};
830
  const stableEvents=mergeStableEvents(String(p.run_id||view.run_id||state.runId||''),p.events||[]);
 
 
831
  const stable=stabilizeProgressPayload(p,view);
832
- const progress=Number(stable.progress??(header.status==='succeeded'?100:0));
833
- const runStatus=p.status||header.status;
834
- const visualRunStatus=p.visual_status||runStatus;
835
- animateProgressBar(progress,visualRunStatus,stable.currentStep);
 
 
836
  setText('progressPercent',`${Math.round(progress)}%`);
837
- setText('progressTitle',header.title||view.run_id||'Live job progress');
 
838
  const statusEl=$('progressStatus');
839
  if(statusEl){
840
- statusEl.textContent=badgeLabel(runStatus||'Running');
841
  statusEl.className='badge '+statusClass(visualRunStatus);
842
  animateStatusBadge(visualRunStatus);
843
  }
844
  const latestEvent=stableEvents[stableEvents.length-1];
845
- setText('currentStep',stable.currentStepLabel||p.current_step_label||'β€”');
846
- setText('lastEvent',eventMessage(latestEvent)||p.last_event||'Waiting for worker events.');
 
847
  setElapsedClock(p.elapsed_seconds??header.elapsed_seconds,runStatus);
848
  setText('lastPolled',new Date().toLocaleTimeString());
849
  setText('detailSpace',header.space||'β€”');
@@ -857,7 +1062,7 @@ function renderRunViewModel(view,p={}){
857
  cancel.hidden=Boolean(terminal);
858
  cancel.disabled=!(state.runId&&view.links?.job_url&&!terminal);
859
  }
860
- renderTimeline(stable.timeline,runStatus);
861
  renderActivityFeed(stableEvents);
862
  renderDiagnostics(view.diagnostics||{},header);
863
  renderSpaceTestPreview(view.space_test||{});
@@ -875,6 +1080,7 @@ function renderProgress(p){
875
  const progress=Number(stable.progress||0);
876
  animateProgressBar(progress,p.visual_status||p.status,stable.currentStep);
877
  setText('progressPercent',`${Math.round(progress)}%`);
 
878
  const latestEvent=stableEvents[stableEvents.length-1];
879
  setText('currentStep',stable.currentStepLabel||p.current_step_label||'β€”');
880
  setText('lastEvent',eventMessage(latestEvent)||p.last_event||'β€”');
@@ -892,6 +1098,11 @@ function renderProgress(p){
892
  }
893
 
894
 
 
 
 
 
 
895
  // Legacy test anchors retained for timeline invariants:
896
  // currentStepLabel:activeItem?.label
897
  // x.done_count||0
@@ -909,3 +1120,8 @@ function renderProgress(p){
909
  // status='after_fail'
910
  // root.scrollLeft-(bounds.rootLeft+margin-bounds.targetLeft)
911
  // root.scrollLeft+(bounds.targetRight-(bounds.rootRight-margin))
 
 
 
 
 
 
270
  if(status==='done'||status==='completed')return'βœ“';
271
  if(status==='running')return'β€’';
272
  if(status==='failed')return'!';
273
+ if(status==='warning'||status==='warn')return'!';
274
  if(status==='fallback')return'⇄';
275
  if(status==='recovered'||status==='superseded')return'β†Ί';
276
  if(status==='blocked'||status==='cancelled'||status==='stopped'||status==='after_fail')return'β– ';
 
283
  const stopped=overall&&PROGRESS_TERMINAL_STATUSES.has(overall)&&!['success','succeeded','done','full_inference_success','full_inference_candidate_health_passed','health_only','partial','partial_validation','completed_with_warnings'].includes(overall);
284
  if(status==='failed'||status==='error'||status==='failure')return'status-failed';
285
  if(status==='fallback')return'status-fallback';
286
+ if(status==='warning'||status==='warn')return'status-warn';
287
  if(status==='recovered'||status==='superseded')return'status-warn';
288
  if(status==='after_fail'||status==='post_failure')return'status-after-fail';
289
  if(status==='blocked'||status==='cancelled'||status==='canceled'||status==='stopped')return'status-stopped';
 
335
  const step=String(event.step||'').toLowerCase();
336
  const status=String(event.status||'').toLowerCase();
337
  const msg=String(event.message||'').trim();
338
+ if(step==='anonymous_eval'&&status==='success'){
339
+ return 'Wrote local eval record';
340
+ }
341
  if(step==='done'&&(status==='full_inference_candidate_health_passed'||status==='health_only'||status==='partial_validation')){
342
  return 'Universal model-card builder completed Β· Partial validation';
343
  }
 
349
  function eventMessage(event){
350
  return friendlyTerminalEventMessage(event||{});
351
  }
352
+
353
+ function hasCanonicalTimelineModel(model){
354
+ return Boolean(model&&model.schema_version==='run_timeline_model.v1'&&Array.isArray(model.phases));
355
+ }
356
+ function canonicalPhaseToTimelineItem(phase={}){
357
+ const raw=String(phase.status||'pending').toLowerCase();
358
+ let status=raw;
359
+ if(raw==='complete')status='done';
360
+ if(raw==='warning')status='warning';
361
+ if(raw==='failed')status='failed';
362
+ if(raw==='stopped')status='stopped';
363
+ if(raw==='not_needed')status='skipped';
364
+ const details=Array.isArray(phase.details)?phase.details:[];
365
+ return {
366
+ id:phase.id||phase.label,
367
+ step:phase.id||phase.label,
368
+ label:phase.label||phase.id||'Phase',
369
+ status,
370
+ summary:phase.summary||'',
371
+ details,
372
+ percent:status==='done'||status==='warning'||status==='skipped'?100:(status==='running'?50:0),
373
+ ring_progress:status==='done'||status==='warning'||status==='skipped'?100:(status==='running'?50:0),
374
+ };
375
+ }
376
+ function timelinePhaseById(phases=[],id=''){
377
+ return phases.find(p=>String(p.id||'')===id)||null;
378
+ }
379
+ function firstPhaseWithStatus(phases=[],statuses=[]){
380
+ const wanted=new Set(statuses.map(s=>String(s||'').toLowerCase()));
381
+ return phases.find(p=>wanted.has(String(p.status||'').toLowerCase()))||null;
382
+ }
383
+ function firstPhaseWithIdsAndStatus(phases=[],ids=[],statuses=[]){
384
+ const wantedIds=new Set(ids);
385
+ const wantedStatuses=new Set(statuses.map(s=>String(s||'').toLowerCase()));
386
+ return phases.find(p=>wantedIds.has(String(p.id||''))&&wantedStatuses.has(String(p.status||'').toLowerCase()))||null;
387
+ }
388
+ function timelineModelActivePhase(model={}){
389
+ const phases=Array.isArray(model.phases)?model.phases:[];
390
+ if(!phases.length)return null;
391
+ const progress=model.progress||{};
392
+ const visual=String(progress.visual_status||'').toLowerCase();
393
+ const terminal=progress.terminal===true||['success','warn','error','stopped'].includes(visual);
394
+ const failed=firstPhaseWithStatus(phases,['failed','stopped']);
395
+ if(failed)return failed;
396
+ if(terminal&&visual==='success'){
397
+ return firstPhaseWithIdsAndStatus(phases,['live_validation'],['complete','warning'])||
398
+ firstPhaseWithIdsAndStatus(phases,['archive'],['complete','warning'])||
399
+ timelinePhaseById(phases,'done')||
400
+ [...phases].reverse().find(p=>String(p.status||'').toLowerCase()!=='pending')||phases[0];
401
+ }
402
+ if(terminal&&visual==='warn'){
403
+ return firstPhaseWithIdsAndStatus(phases,['live_validation','archive','done'],['warning'])||
404
+ firstPhaseWithStatus(phases,['warning'])||
405
+ firstPhaseWithIdsAndStatus(phases,['live_validation','archive','done'],['complete'])||
406
+ [...phases].reverse().find(p=>String(p.status||'').toLowerCase()!=='pending')||phases[0];
407
+ }
408
+ const running=firstPhaseWithStatus(phases,['running']);
409
+ if(running)return running;
410
+ const warning=firstPhaseWithStatus(phases,['warning']);
411
+ if(warning)return warning;
412
+ return [...phases].reverse().find(p=>String(p.status||'').toLowerCase()!=='pending')||phases[0]||null;
413
+ }
414
+ function renderTimelineModelWarnings(model={}){
415
+ const note=$('timelineTraceNote');
416
+ if(!note)return;
417
+ const warnings=Array.isArray(model.warnings)?model.warnings:[];
418
+ if(!warnings.length){note.hidden=true;note.innerHTML='';note.className='timeline-trace-note';return;}
419
+ const first=warnings[0]||{};
420
+ const extra=warnings.length>1?` +${warnings.length-1} more`:'';
421
+ note.hidden=false;
422
+ note.className='timeline-trace-note fallback canonical-warning';
423
+ note.innerHTML=`<strong><span>⚠</span>${escapeHtml(first.label||'Run warning')}${escapeHtml(extra)}</strong><p>${escapeHtml(first.detail||'This run completed with non-blocking warnings.')}</p>`;
424
+ }
425
+ function timelineDetailStatusLabel(status=''){
426
+ const s=String(status||'').toLowerCase();
427
+ if(s==='complete'||s==='done'||s==='success')return'Complete';
428
+ if(s==='warning'||s==='warn')return'Needs attention';
429
+ if(s==='running'||s==='waiting')return'In progress';
430
+ if(s==='not_needed'||s==='skipped')return'Not needed';
431
+ if(s==='failed'||s==='error')return'Failed';
432
+ if(s==='stopped'||s==='cancelled'||s==='blocked')return'Stopped';
433
+ return badgeLabel(s||'pending');
434
+ }
435
+ function timelinePhaseDetailRows(phase={}){
436
+ const details=Array.isArray(phase.details)?phase.details.filter(Boolean):[];
437
+ if(details.length){
438
+ return details.slice(0,6).map(d=>{
439
+ const label=escapeHtml(d.label||d.title||'Detail');
440
+ const value=escapeHtml(d.value||d.detail||d.summary||d.status||'');
441
+ const status=String(d.status||'').toLowerCase();
442
+ const cls=status?` ${statusClass(status)}`:'';
443
+ return `<li class="timeline-detail-row${cls}"><span>${label}</span>${value?`<strong>${value}</strong>`:''}</li>`;
444
+ }).join('');
445
+ }
446
+ const summary=String(phase.summary||'').trim();
447
+ return summary?`<li class="timeline-detail-row neutral"><span>Summary</span><strong>${escapeHtml(summary)}</strong></li>`:'';
448
+ }
449
+ function renderTimelineModelDetails(model={},activePhase=null){
450
+ const root=$('timelinePhaseDetails');
451
+ if(!root)return;
452
+ if(!hasCanonicalTimelineModel(model)){root.hidden=true;root.innerHTML='';root.className='timeline-phase-details';return;}
453
+ const phase=activePhase||timelineModelActivePhase(model)||{};
454
+ if(!phase||!(phase.id||phase.label)){root.hidden=true;root.innerHTML='';return;}
455
+ const status=String(phase.status||'pending').toLowerCase();
456
+ const tone=timelineStatus(status).replace(/^status-/,'');
457
+ const rows=timelinePhaseDetailRows(phase);
458
+ const warnings=Array.isArray(model.warnings)?model.warnings:[];
459
+ const warningHtml=warnings.length?`<div class="timeline-detail-warnings"><strong>Needs attention</strong>${warnings.slice(0,3).map(w=>`<p>${escapeHtml(w.label||'Warning')}${w.detail?`: ${escapeHtml(w.detail)}`:''}</p>`).join('')}</div>`:'';
460
+ root.hidden=false;
461
+ root.className=`timeline-phase-details ${tone}`;
462
+ root.innerHTML=`<div class="timeline-detail-head"><span>${escapeHtml(phase.label||'Phase')}</span><strong>${escapeHtml(timelineDetailStatusLabel(status))}</strong></div>${phase.summary?`<p>${escapeHtml(phase.summary)}</p>`:''}${rows?`<ul>${rows}</ul>`:''}${warningHtml}`;
463
+ }
464
+ function renderTimelineModel(model={},overallStatus=''){
465
+ const root=$('timeline');
466
+ if(!root)return false;
467
+ if(!hasCanonicalTimelineModel(model))return false;
468
+ if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{if(root.dataset.autoScrolling==='1')return;root.dataset.userScrolled='1'},{passive:true});}
469
+ const list=(model.phases||[]).map(canonicalPhaseToTimelineItem);
470
+ const wideTimeline=typeof window==='undefined'||!window.matchMedia||window.matchMedia('(min-width: 721px)').matches;
471
+ root.style.gridTemplateColumns=wideTimeline&&list.length?`repeat(${list.length}, minmax(104px, 1fr))`:'';
472
+ root.classList.add('timeline-canonical');
473
+ root.classList.remove('trace-failed','trace-recovered','trace-fallback');
474
+ renderTimelineModelWarnings(model);
475
+ const active=timelineModelActivePhase(model)||{};
476
+ renderTimelineModelDetails(model,active);
477
+ const currentKey=String(active.id||active.label||'');
478
+ const signature=list.map((x,i)=>`${timelineStepKey(x,i)}:${x.status}:${x.label}:${x.summary}:${overallStatus}`).join('|');
479
+ if(root.dataset.signature===signature){
480
+ if(root.dataset.visibleStep!==currentKey&&root.dataset.userScrolled!=='1')ensureTimelineStepVisible(currentKey);
481
+ return true;
482
+ }
483
+ const previousKey=root.dataset.currentStep||'';
484
+ if(previousKey!==currentKey)root.dataset.userScrolled='';
485
+ root.dataset.signature=signature;
486
+ root.dataset.currentStep=currentKey||'';
487
+ const nextState={};
488
+ root.innerHTML=list.map((x,i)=>{
489
+ const key=timelineStepKey(x,i);
490
+ const st=timelineStatus(x.status,overallStatus);
491
+ const prev=timelineRenderState[key];
492
+ const changed=prev&&prev!==st;
493
+ nextState[key]=st;
494
+ const label=st.replace(/^status-/,'');
495
+ const summary=x.summary||label;
496
+ const details=(x.details||[]).map(d=>d&&d.label).filter(Boolean);
497
+ const detailTitle=details.length?details.join(' Β· '):summary;
498
+ const title=` title="${escapeHtml([x.label,summary,detailTitle].filter(Boolean).join(' Β· '))}"`;
499
+ const reasonLine=summary?`<small class="step-reason canonical-summary">${escapeHtml(summary)}</small>`:'';
500
+ const aria=`${x.label}: ${label}${summary?`, ${summary}`:''}`;
501
+ return `<div class="step ${st} canonical-phase${changed?' status-changed':''}" data-step="${escapeHtml(key)}" data-status="${label}" aria-label="${escapeHtml(aria)}" style="--step-progress:${Number(x.percent||0)}%"${title}><div class="step-dot"><span>${timelineIcon(x.status)}</span></div><div><strong>${escapeHtml(x.label)}</strong><small>${escapeHtml(summary)}</small>${reasonLine}</div></div>`;
502
+ }).join('');
503
+ Object.keys(timelineRenderState).forEach(k=>delete timelineRenderState[k]);
504
+ Object.assign(timelineRenderState,nextState);
505
+ root.querySelectorAll('.step.status-changed').forEach(step=>{animateClass(step,'status-flash',1000);animateClass(step.querySelector('.step-dot'),'dot-pop',700);});
506
+ if(!root.dataset.userScrolled||previousKey!==currentKey){root.dataset.autoScrolling='1';const scrollOpts={instant:!root.dataset.visibleStep,force:previousKey!==currentKey};(typeof requestAnimationFrame==='function'?requestAnimationFrame:(fn)=>setTimeout(fn,0))(()=>(typeof requestAnimationFrame==='function'?requestAnimationFrame:(fn)=>setTimeout(fn,0))(()=>ensureTimelineStepVisible(currentKey,scrollOpts)));setTimeout(()=>{if(root)root.dataset.autoScrolling=''},360);}
507
+ return true;
508
+ }
509
  function timelineStepBounds(root,target){
510
  const rootBox=root.getBoundingClientRect?root.getBoundingClientRect():{left:0,right:root.clientWidth,width:root.clientWidth};
511
  const targetBox=target.getBoundingClientRect?target.getBoundingClientRect():{left:target.offsetLeft-root.scrollLeft,right:target.offsetLeft-root.scrollLeft+target.offsetWidth,width:target.offsetWidth};
 
684
  const root=$('timeline');
685
  if(!root)return;
686
  if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{if(root.dataset.autoScrolling==='1')return;root.dataset.userScrolled='1'},{passive:true});}
687
+ root.classList.remove('timeline-canonical');
688
+ const canonicalDetails=$('timelinePhaseDetails');
689
+ if(canonicalDetails){canonicalDetails.hidden=true;canonicalDetails.innerHTML='';canonicalDetails.className='timeline-phase-details';}
690
  const fullList=items||[];
691
  const list=compactTimelineItems(fullList,overallStatus);
692
  // Keep the compact timeline on a single row. The number of visible groups is
 
877
  let klass=visualStatusClass(status);if(value>=100&&klass==='neutral')klass='success';
878
  fill.style.setProperty('--progress-target',`${value}%`);
879
  fill.style.width=`${value}%`;
880
+ fill.classList.remove('is-success','is-warn','is-error','is-running-state','is-stopped','is-neutral');
881
  fill.classList.add(`is-${klass==='running'?'running-state':klass}`);
882
  const s=String(status||'').toLowerCase();fill.classList.toggle('is-running',klass==='running'||s==='pending'||s==='scheduled'||s==='waiting');
883
  if(previous!==null&&Math.round(previous)!==Math.round(value)){
 
897
  }
898
  progressMotionState.lastStatus=next;
899
  }
900
+
901
+ function progressProcessLabel(progress={},percent=0){
902
+ const terminal=Boolean(progress.terminal);
903
+ const status=String(progress.process_status||'').toLowerCase();
904
+ if(terminal||Number(percent)>=100)return'Completed';
905
+ if(status.includes('running')||status.includes('waiting')||status.includes('pending'))return'In progress';
906
+ return status?badgeLabel(status):'In progress';
907
+ }
908
+ function progressVerdictLabel(progress={},visualStatus=''){
909
+ const label=String(progress.label||'').trim();
910
+ if(label)return label;
911
+ const verdict=String(progress.verdict||visualStatus||'').trim();
912
+ return verdict?badgeLabel(verdict):'Pending verdict';
913
+ }
914
+ function renderSemanticProgressSummary(progress={},percent=0,visualStatus=''){
915
+ const root=$('progressSemantics');
916
+ const caption=$('progressCaptionLabel');
917
+ const bar=$('progressBar');
918
+ if(caption)caption.textContent=progress&&progress.verdict?'Process progress':'Overall progress';
919
+ if(bar){
920
+ const value=Math.max(0,Math.min(100,Number(percent||0)));
921
+ bar.setAttribute('aria-valuenow',String(Math.round(value)));
922
+ bar.setAttribute('aria-label',progress&&progress.verdict?`Process progress ${Math.round(value)} percent, result ${progressVerdictLabel(progress,visualStatus)}`:`Overall progress ${Math.round(value)} percent`);
923
+ }
924
+ if(!root)return;
925
+ if(!progress||!progress.verdict){root.hidden=true;root.innerHTML='';return;}
926
+ const process=progressProcessLabel(progress,percent);
927
+ const verdict=progressVerdictLabel(progress,visualStatus);
928
+ const tone=visualStatusClass(visualStatus||progress.visual_status||progress.verdict);
929
+ const subtitle=String(progress.subtitle||'').trim();
930
+ root.hidden=false;
931
+ root.className=`progress-semantics ${tone}`;
932
+ root.innerHTML=`<span><strong>Process</strong> ${escapeHtml(process)}</span><span><strong>Result</strong> ${escapeHtml(verdict)}</span>${subtitle?`<em>${escapeHtml(subtitle)}</em>`:''}`;
933
+ }
934
  function setElapsedClock(seconds,status){
935
  const s=String(status||'').toLowerCase();
936
  elapsedClock.baseSeconds=Math.max(0,Number(seconds||0));
 
975
  progressMotionState.lastStatus=null;
976
  progressMotionState.lastStep=null;
977
  const timeline=$('timeline');
978
+ if(timeline){timeline.dataset.signature='';timeline.dataset.visibleStep='';timeline.dataset.userScrolled='';timeline.classList.remove('trace-failed','trace-recovered','trace-fallback','timeline-canonical');timeline.scrollLeft=0;}const note=$('timelineTraceNote');if(note){note.hidden=true;note.innerHTML='';note.className='timeline-trace-note';}const details=$('timelinePhaseDetails');if(details){details.hidden=true;details.innerHTML='';details.className='timeline-phase-details';}
979
  }
980
  function stabilizeProgressPayload(p={},view=null){
981
  const runId=String(p.run_id||view?.run_id||state.runId||'');
 
1027
  const header=view.header||{};
1028
  const sm=view.status_model||{};
1029
  const stableEvents=mergeStableEvents(String(p.run_id||view.run_id||state.runId||''),p.events||[]);
1030
+ const timelineModel=view.timeline_model||p.timeline_model||null;
1031
+ const modelProgress=hasCanonicalTimelineModel(timelineModel)?(timelineModel.progress||{}):null;
1032
  const stable=stabilizeProgressPayload(p,view);
1033
+ const progress=Number(modelProgress?.percent??stable.progress??(header.status==='succeeded'?100:0));
1034
+ const runStatus=modelProgress?.verdict||p.status||header.status;
1035
+ const visualRunStatus=modelProgress?.visual_status||p.visual_status||runStatus;
1036
+ const activePhase=hasCanonicalTimelineModel(timelineModel)?timelineModelActivePhase(timelineModel):null;
1037
+ // Legacy invariant: animateProgressBar(progress,visualRunStatus,stable.currentStep) remains true for non-canonical payloads.
1038
+ animateProgressBar(progress,visualRunStatus,activePhase?.id||stable.currentStep);
1039
  setText('progressPercent',`${Math.round(progress)}%`);
1040
+ renderSemanticProgressSummary(modelProgress,progress,visualRunStatus);
1041
+ setText('progressTitle',modelProgress?.label||header.title||view.run_id||'Live job progress');
1042
  const statusEl=$('progressStatus');
1043
  if(statusEl){
1044
+ statusEl.textContent=modelProgress?.label||badgeLabel(runStatus||'Running');
1045
  statusEl.className='badge '+statusClass(visualRunStatus);
1046
  animateStatusBadge(visualRunStatus);
1047
  }
1048
  const latestEvent=stableEvents[stableEvents.length-1];
1049
+ // Legacy invariant: setText('currentStep',stable.currentStepLabel remains the fallback path when no canonical phase is available.
1050
+ setText('currentStep',activePhase?`${activePhase.label}: ${activePhase.summary||activePhase.status}`:(stable.currentStepLabel||p.current_step_label||'β€”'));
1051
+ setText('lastEvent',modelProgress?.subtitle||eventMessage(latestEvent)||p.last_event||'Waiting for worker events.');
1052
  setElapsedClock(p.elapsed_seconds??header.elapsed_seconds,runStatus);
1053
  setText('lastPolled',new Date().toLocaleTimeString());
1054
  setText('detailSpace',header.space||'β€”');
 
1062
  cancel.hidden=Boolean(terminal);
1063
  cancel.disabled=!(state.runId&&view.links?.job_url&&!terminal);
1064
  }
1065
+ if(!renderTimelineModel(timelineModel,runStatus))renderTimeline(stable.timeline,runStatus);
1066
  renderActivityFeed(stableEvents);
1067
  renderDiagnostics(view.diagnostics||{},header);
1068
  renderSpaceTestPreview(view.space_test||{});
 
1080
  const progress=Number(stable.progress||0);
1081
  animateProgressBar(progress,p.visual_status||p.status,stable.currentStep);
1082
  setText('progressPercent',`${Math.round(progress)}%`);
1083
+ renderSemanticProgressSummary(null,progress,p.visual_status||p.status);
1084
  const latestEvent=stableEvents[stableEvents.length-1];
1085
  setText('currentStep',stable.currentStepLabel||p.current_step_label||'β€”');
1086
  setText('lastEvent',eventMessage(latestEvent)||p.last_event||'β€”');
 
1098
  }
1099
 
1100
 
1101
+
1102
+ // v189.2 compatibility anchors for older progress tests:
1103
+ // const visualRunStatus=p.visual_status||runStatus
1104
+ // setText('currentStep',stable.currentStepLabel
1105
+
1106
  // Legacy test anchors retained for timeline invariants:
1107
  // currentStepLabel:activeItem?.label
1108
  // x.done_count||0
 
1120
  // status='after_fail'
1121
  // root.scrollLeft-(bounds.rootLeft+margin-bounds.targetLeft)
1122
  // root.scrollLeft+(bounds.targetRight-(bounds.rootRight-margin))
1123
+
1124
+ // v189.3 semantic progress anchors:
1125
+ // renderSemanticProgressSummary(modelProgress,progress,visualRunStatus)
1126
+ // progressCaptionLabel = Process progress when timeline_model progress verdict is present
1127
+ // progress bar color follows visual_status, so 100% warn is not success
web/static/runs.js CHANGED
@@ -652,6 +652,7 @@ function defaultRunDocuments(detail={}){
652
  {id:'repair_decision',label:'Decision',subtitle:'Source JSON',icon:'β—‡',present:false,url:blob('repair/REPAIR_DECISION.json')},
653
  {id:'blockage',label:'Blockage',subtitle:'Source JSON',icon:'!',tone:'warn',present:false,url:blob('repair/BLOCKAGE.json')},
654
  {id:'eval_publish',label:'Eval publish',subtitle:'Archive status',icon:'β—Ž',present:false,url:blob('eval_publish_status.json')},
 
655
  ];
656
  }
657
  function mergeStableRunDocuments(runId,docs=[]){
@@ -711,15 +712,25 @@ function deriveRunDocuments(detail={}){
711
  if(byId.has('eval_publish')){
712
  byId.get('eval_publish').present=Boolean((byId.get('eval_publish').present&&byId.get('eval_publish').url)||nonEmptyObject(detail.eval_publish)||nonEmptyObject(detail.eval_publish_status)||filePaths.has('eval_publish_status.json'));
713
  }
 
 
 
 
 
 
 
 
 
 
714
  if(!byId.has('repair')&&hasPrefix('repair')){
715
  const runId=summary.run_id||detail.run_id||state.runId;
716
  const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;
717
  byId.set('repair',{id:'repair',label:'Repair',subtitle:'Source folder',icon:'✦',tone:status.includes('failed')?'warn':'neutral',present:true,url:runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/repair`:''});
718
  }
719
- const order=['pi_raw_trace','pi_redacted_trace','repair_decision','repair','blockage','report','smoke','eval_publish','blockers'];
720
  const runId=String(summary.run_id||detail.run_id||state.runId||'');
721
  const resolved=order.map(id=>byId.get(id)).filter(Boolean).filter(d=>{
722
- if(['repair_decision','blockage','eval_publish'].includes(d.id))return Boolean(d.present);
723
  if(d.id==='blockers')return shouldShowBlockers||d.present;
724
  return true;
725
  });
 
652
  {id:'repair_decision',label:'Decision',subtitle:'Source JSON',icon:'β—‡',present:false,url:blob('repair/REPAIR_DECISION.json')},
653
  {id:'blockage',label:'Blockage',subtitle:'Source JSON',icon:'!',tone:'warn',present:false,url:blob('repair/BLOCKAGE.json')},
654
  {id:'eval_publish',label:'Eval publish',subtitle:'Archive status',icon:'β—Ž',present:false,url:blob('eval_publish_status.json')},
655
+ {id:'space_logs',label:'Space logs',subtitle:'Build/runtime index',icon:'β—‰',present:false,url:blob('logs/space_logs_index.json')},
656
  ];
657
  }
658
  function mergeStableRunDocuments(runId,docs=[]){
 
712
  if(byId.has('eval_publish')){
713
  byId.get('eval_publish').present=Boolean((byId.get('eval_publish').present&&byId.get('eval_publish').url)||nonEmptyObject(detail.eval_publish)||nonEmptyObject(detail.eval_publish_status)||filePaths.has('eval_publish_status.json'));
714
  }
715
+ if(byId.has('space_logs')){
716
+ const logIndex=detail.space_logs_index||detail.space_log_index||detail.logs_index||{};
717
+ const hasSpaceLogs=nonEmptyObject(logIndex)||filePaths.has('logs/space_logs_index.json')||filePaths.has('space_logs_index.json')||filePaths.has('logs/space_log_diagnostics.json')||filePaths.has('logs/space_runtime_snapshot.json');
718
+ byId.get('space_logs').present=Boolean((byId.get('space_logs').present&&byId.get('space_logs').url)||hasSpaceLogs);
719
+ if(nonEmptyObject(logIndex)){
720
+ const quality=String(logIndex.log_quality||logIndex.quality||'').toLowerCase();
721
+ if(quality==='snapshot_only'||quality==='partial')byId.get('space_logs').tone='warn';
722
+ if(quality==='unavailable')byId.get('space_logs').tone='warn';
723
+ }
724
+ }
725
  if(!byId.has('repair')&&hasPrefix('repair')){
726
  const runId=summary.run_id||detail.run_id||state.runId;
727
  const bucket=summary.bucket_source||detail.bucket_source||state.bucketSource;
728
  byId.set('repair',{id:'repair',label:'Repair',subtitle:'Source folder',icon:'✦',tone:status.includes('failed')?'warn':'neutral',present:true,url:runId&&bucket?`https://huggingface.co/buckets/${bucket}/tree/runs/${runId}/repair`:''});
729
  }
730
+ const order=['pi_raw_trace','pi_redacted_trace','repair_decision','repair','blockage','report','smoke','eval_publish','space_logs','blockers'];
731
  const runId=String(summary.run_id||detail.run_id||state.runId||'');
732
  const resolved=order.map(id=>byId.get(id)).filter(Boolean).filter(d=>{
733
+ if(['repair_decision','blockage','eval_publish','space_logs'].includes(d.id))return Boolean(d.present);
734
  if(d.id==='blockers')return shouldShowBlockers||d.present;
735
  return true;
736
  });