| const timelineRenderState={}; |
| const activityFeedState={lastItems:[],runId:null}; |
| const PROGRESS_TERMINAL_STATUSES=new Set(['done','success','failed','full_inference_success','full_inference_candidate_health_passed','manual_hardware_required','generated_needs_manual_hardware','technical_blocker','technical_blocker_boot_only','health_only','partial','partial_validation','completed_with_warnings','demo_usable_full_promise_not_verified','interactive_app_available_smoke_failed','manual_test_required_smoke_failed','validated','validated_after_space_test','validated_after_manual_space_test','manual_validation_passed','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled','stopped','auth_refresh_required']); |
| const PROGRESS_BLOCKING_TERMINAL_STATUSES=new Set(['failed','failure','error','technical_blocker','technical_blocker_boot_only','blocked','manual_hardware_required','generated_needs_manual_hardware','waiting_manual_action','auth_refresh_required','stale','stopped','cancelled','canceled']); |
|
|
| const WORKER_STEP_ORDER=[ |
| 'bootstrap','dependencies','auth','model_analysis','workspace','node','pi_install','pi_config','pi_run','pi_model_resolution','pi_verification', |
| 'metadata_sanitize','requirements_sanitize','hardware_strategy','create_space_hardware','create_space','failure_detected','failure_diagnosis','pi_diagnosis','repair_decision','wait_for_logs','factory_rebuild','repair','repair_diagnosis','repair_brief','repair_plan','repair_patch','repair_upload','repair_validation','technical_blocker','manual_hardware_required','upload_files', |
| 'space_runtime','space_logs','api_validation','live_wait','generation_smoke','inference_gate','report_write','done','failure' |
| ]; |
|
|
|
|
| const COMPACT_TIMELINE_GROUPS=[ |
| {id:'bootstrap',label:'Bootstrap',display_label:'Start',steps:['bootstrap']}, |
| {id:'dependencies',label:'Dependencies',display_label:'Dependencies',steps:['dependencies']}, |
| {id:'auth_model',label:'Auth + model',display_label:'Model access',steps:['auth','model_analysis']}, |
| {id:'workspace_node',label:'Workspace/node',display_label:'Workspace',steps:['workspace','node']}, |
| {id:'pi_setup',label:'Pi setup',display_label:'Agent setup',steps:['pi_install','pi_config']}, |
| {id:'pi_run',label:'Pi run',display_label:'Agent build',steps:['pi_run']}, |
| {id:'pi_verify',label:'Pi verify',display_label:'Agent review',steps:['pi_model_resolution','pi_verification','metadata_sanitize','requirements_sanitize']}, |
| {id:'hardware',label:'Hardware',display_label:'Hardware',steps:['hardware_strategy','create_space_hardware']}, |
| {id:'create_space',label:'Create Space',display_label:'Space',steps:['create_space']}, |
| {id:'upload_runtime',label:'Upload/runtime',display_label:'Deploy',steps:['upload_files','space_runtime','space_logs','live_wait']}, |
| {id:'validation',label:'Validation',display_label:'Live test',steps:['api_validation','generation_smoke','inference_gate']}, |
| {id:'repair',label:'Repair recovery',display_label:'Recovery',steps:['failure_detected','failure_diagnosis','pi_diagnosis','repair_decision','wait_for_logs','factory_rebuild','repair','repair_diagnosis','repair_brief','repair_plan','repair_patch','repair_upload','repair_validation','technical_blocker','manual_hardware_required']}, |
| {id:'report',label:'Report',display_label:'Report',steps:['report_write','done']}, |
| {id:'failure',label:'Failure',display_label:'Failure',terminal_only:true,steps:['failure']} |
| ]; |
|
|
| const progressMotionState={lastProgress:null,lastStatus:null,lastStep:null}; |
|
|
| const WORKER_STEP_DISPLAY_LABELS={ |
| bootstrap:'start',dependencies:'dependencies',auth:'auth',model_analysis:'model card',workspace:'workspace',node:'node setup', |
| pi_install:'agent install',pi_config:'agent config',pi_run:'agent building',pi_model_resolution:'model routing',pi_verification:'agent check', |
| metadata_sanitize:'metadata check',requirements_sanitize:'requirements check',hardware_strategy:'hardware plan',create_space_hardware:'hardware request', |
| create_space:'space created',upload_files:'files uploaded',space_runtime:'runtime check',space_logs:'logs checked',live_wait:'space live', |
| api_validation:'API check',generation_smoke:'live generation',inference_gate:'inference verdict',failure_detected:'failure detected', |
| failure_diagnosis:'diagnosis',pi_diagnosis:'agent diagnosis',repair_decision:'decision',wait_for_logs:'waiting for logs', |
| factory_rebuild:'same-code rebuild',repair:'repair started',repair_diagnosis:'patch allowed',repair_brief:'repair brief',repair_plan:'repair plan', |
| repair_patch:'patching',repair_upload:'repair uploaded',repair_validation:'repair test',technical_blocker:'blocker',manual_hardware_required:'manual hardware', |
| report_write:'report written',done:'done',failure:'failed' |
| }; |
| function workerStepDisplayLabel(step){return WORKER_STEP_DISPLAY_LABELS[String(step||'')]||String(step||'').replace(/_/g,' ')} |
|
|
| const progressStability={runId:null,progress:0,timeline:null,furthestIndex:-1,status:'',currentStep:null}; |
| const elapsedClock={baseSeconds:0,baseAt:0,isRunning:false}; |
|
|
| function isFailureStatus(status){ |
| const s=String(status||'').toLowerCase(); |
| return s.includes('failed')||s.includes('error')||s==='failure'||s==='technical_blocker'||s==='technical_blocker_boot_only'||s==='blocked'; |
| } |
| function isSuccessStatus(status){ |
| const s=String(status||'').toLowerCase(); |
| if(PROGRESS_BLOCKING_TERMINAL_STATUSES.has(s)||s.includes('technical_blocker')||s.includes('blocked')||s.includes('failed')||s.includes('error'))return false; |
| return s.includes('success')||s.includes('succeed')||s.includes('passed')||s==='done'||s==='completed'||s==='repair_success'||s==='full_inference_success'; |
| } |
| function isPartialStatus(status){ |
| const s=String(status||'').toLowerCase(); |
| return s==='full_inference_candidate_health_passed'||s==='health_only'||s==='partial'||s==='partial_validation'||s==='completed_with_warnings'||s==='demo_usable_full_promise_not_verified'||s==='interactive_app_available_smoke_failed'||s==='manual_test_required_smoke_failed'; |
| } |
| function isStoppedStatus(status){ |
| const s=String(status||'').toLowerCase(); |
| return s==='cancelled'||s==='canceled'||s==='stopped'||s==='blocked'||s==='stale'; |
| } |
| function visualStatusClass(status){ |
| if(isFailureStatus(status))return'error'; |
| if(isPartialStatus(status))return'warn'; |
| if(isStoppedStatus(status))return'stopped'; |
| if(isSuccessStatus(status))return'success'; |
| const s=String(status||'').toLowerCase(); |
| if(s.includes('running')||s.includes('queued')||s.includes('pending')||s.includes('waiting')||s.includes('building'))return'running'; |
| return'neutral'; |
| } |
| function groupForWorkerStep(step){ |
| return COMPACT_TIMELINE_GROUPS.find(g=>g.steps.includes(String(step||'')))||null; |
| } |
| function latestEventIndexForGroup(group){ |
| if(!group)return -1; |
| let idx=-1; |
| (activityFeedState.lastItems||[]).forEach((event,i)=>{ |
| if(group.steps.includes(String(event.step||'')))idx=i; |
| }); |
| return idx; |
| } |
| function failedEventInfoForGroup(group){ |
| if(!group)return null; |
| let info=null; |
| (activityFeedState.lastItems||[]).forEach((event,i)=>{ |
| if(group.steps.includes(String(event.step||''))&&isFailureStatus(event.status)){ |
| info={event,index:i}; |
| } |
| }); |
| return info; |
| } |
| function failedItemInfoForGroup(group,byStep){ |
| if(!group||!byStep)return null; |
| let info=null; |
| group.steps.forEach((step,i)=>{ |
| const item=byStep.get(step); |
| if(item&&isFailureStatus(item.status))info={event:item,index:i,fromTimeline:true}; |
| }); |
| return info; |
| } |
| function textOfEvent(event={}){ |
| return `${event.step||''} ${event.status||''} ${event.message||''} ${event.hardware||''} ${event.selected_hardware||''} ${event.fallback_hardware||''}`.toLowerCase(); |
| } |
| function isHardwareFallbackEvent(event={}){ |
| const text=textOfEvent(event); |
| return text.includes('fallback')||text.includes('zerogpu')||text.includes('zero gpu')||text.includes('zero-a10g')||text.includes('hardware_fallback')||text.includes('a10g')||text.includes('l40')||text.includes('h200'); |
| } |
| function groupHasHardwareFallback(group){ |
| if(!group||group.id!=='hardware')return false; |
| return (activityFeedState.lastItems||[]).some(event=>group.steps.includes(String(event.step||''))&&isHardwareFallbackEvent(event)); |
| } |
| function isExpectedFallbackFailure(group,failed){ |
| return Boolean(group&&group.id==='hardware'&&failed&&isHardwareFallbackEvent(failed.event)); |
| } |
| function humanizeHardwareReason(text){ |
| const raw=String(text||'').replace(/\s+/g,' ').trim(); |
| const zero=raw.match(/ZeroGPU Spaces are currently limited to\s*(\d+)\s*per user,?\s*and there are already\s*(\d+)\s*Spaces running on ZeroGPU/i); |
| if(zero){ |
| return `ZeroGPU unavailable: namespace limit reached. ${zero[2]} ZeroGPU Spaces are already running; current limit is ${zero[1]}.`; |
| } |
| return raw; |
| } |
| function compactReasonText(value,max=190){ |
| let text=String(value||'').replace(/\s+/g,' ').trim(); |
| if(!text)return ''; |
| if(text.startsWith('{')){ |
| try{ |
| const parsed=JSON.parse(text); |
| text=parsed.server_message||parsed.hf_error_message||parsed.error||parsed.message||text; |
| }catch(_){} |
| } |
| text=text.replace(/^Bad request:\s*/i,'').trim(); |
| text=text.replace(/^\(Request ID:[^)]+\)\s*/i,'').trim(); |
| text=humanizeHardwareReason(text); |
| return text.length>max?text.slice(0,max-1)+'…':text; |
| } |
| function eventReason(event={}){ |
| const data=event.data||{}; |
| const raw= |
| event.reason|| |
| event.error|| |
| event.error_message|| |
| event.exception|| |
| event.details|| |
| data.reason|| |
| data.hf_error_detail|| |
| data.hf_error_message|| |
| data.error_detail|| |
| data.response_text|| |
| data.response_body|| |
| data.error|| |
| data.exception|| |
| data.details|| |
| event.message|| |
| ''; |
| return compactReasonText(typeof raw==='string'?raw:JSON.stringify(raw)); |
| } |
| function zeroGpuReason(event={}){ |
| const reason=eventReason(event); |
| const lower=reason.toLowerCase(); |
| if(lower.includes('quota')||lower.includes('limit')||lower.includes('capacity')||lower.includes('usage')||lower.includes('exceeded')){ |
| return reason; |
| } |
| if(isHardwareFallbackEvent(event)){ |
| return reason||'ZeroGPU was unavailable for this user/session, so the fallback GPU was used.'; |
| } |
| return reason; |
| } |
| function failureReasonForGroup(group){ |
| const failed=failedEventInfoForGroup(group); |
| if(failed)return eventReason(failed.event); |
| return ''; |
| } |
| function fallbackReasonForGroup(group){ |
| if(!group||group.id!=='hardware')return ''; |
| const failed=failedEventInfoForGroup(group); |
| if(failed&&isExpectedFallbackFailure(group,failed))return zeroGpuReason(failed.event); |
| const event=(activityFeedState.lastItems||[]).find(e=>group.steps.includes(String(e.step||''))&&isHardwareFallbackEvent(e)); |
| return event?zeroGpuReason(event):''; |
| } |
| function reasonForTimelineItem(item={}){ |
| if(item.status==='fallback')return item.reason||'Fallback GPU route was used.'; |
| if(item.status==='failed'||item.status==='recovered')return item.reason||''; |
| return ''; |
| } |
| function failedTimelineStatusForGroup(group,overallStatus){ |
| const failed=failedEventInfoForGroup(group); |
| if(!failed)return ''; |
| const latestIdx=latestEventIndexForGroup(group); |
| const overall=visualStatusClass(overallStatus); |
| const superseded=latestIdx>failed.index; |
| if(isExpectedFallbackFailure(group,failed)&&superseded)return 'fallback'; |
| if(overall==='error')return 'failed'; |
| return superseded?'recovered':'failed'; |
| } |
| function firstFailedGroupIndex(overallStatus){ |
| const overall=visualStatusClass(overallStatus); |
| let first=-1; |
| COMPACT_TIMELINE_GROUPS.forEach((group,idx)=>{ |
| const failed=failedEventInfoForGroup(group); |
| if(!failed)return; |
| const historical=failedTimelineStatusForGroup(group,overallStatus); |
| if(historical==='fallback')return;if((overall==='error'||historical==='failed')&&(first<0||idx<first))first=idx; |
| }); |
| return first; |
| } |
| function firstProblemGroupKey(items=[],overallStatus=''){ |
| const first=firstFailedGroupIndex(overallStatus); |
| if(first>=0)return COMPACT_TIMELINE_GROUPS[first]?.id||''; |
| const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item])); |
| const groups=visibleCompactTimelineGroups(items,overallStatus); |
| const failedFromTimeline=groups.find(group=>group.steps.some(step=>isFailureStatus(byStep.get(step)?.status))); |
| if(failedFromTimeline)return failedFromTimeline.id||''; |
| if(visualStatusClass(overallStatus)==='error'){ |
| const lastNonPending=[...groups].reverse().find(group=>group.steps.some(step=>byStep.get(step)&&String(byStep.get(step).status||'').toLowerCase()!=='pending')); |
| return lastNonPending?.id||'repair'; |
| } |
| const recovered=COMPACT_TIMELINE_GROUPS.find(group=>failedTimelineStatusForGroup(group,overallStatus)==='recovered'); |
| if(recovered)return recovered.id||''; |
| const fallback=COMPACT_TIMELINE_GROUPS.find(group=>failedTimelineStatusForGroup(group,overallStatus)==='fallback'||groupHasHardwareFallback(group)); |
| return fallback?.id||''; |
| } |
|
|
| function terminalFailureAnchorGroupIndex(items=[],overallStatus=''){ |
| if(visualStatusClass(overallStatus)!=='error')return -1; |
| const events=activityFeedState.lastItems||[]; |
| const groups=visibleCompactTimelineGroups(items,overallStatus).filter(g=>g.id!=='failure'&&g.id!=='report'); |
| const groupIndexById=new Map(groups.map((g,i)=>[g.id,i])); |
| |
| for(const event of [...events].reverse()){ |
| const step=String(event.step||''); |
| if(!step||step==='failure'||!isFailureStatus(event.status))continue; |
| const group=COMPACT_TIMELINE_GROUPS.find(g=>g.id!=='failure'&&g.steps.includes(step)); |
| if(group&&groupIndexById.has(group.id))return groupIndexById.get(group.id); |
| } |
| |
| |
| |
| for(const event of [...events].reverse()){ |
| const step=String(event.step||''); |
| if(!step||step==='failure'||step==='done'||step==='report_write')continue; |
| const group=COMPACT_TIMELINE_GROUPS.find(g=>g.id!=='failure'&&g.id!=='report'&&g.steps.includes(step)); |
| if(group&&groupIndexById.has(group.id))return groupIndexById.get(group.id); |
| } |
| const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item])); |
| for(let i=groups.length-1;i>=0;i--){ |
| const group=groups[i]; |
| if(group.steps.some(step=>byStep.has(step)&&String(byStep.get(step).status||'').toLowerCase()!=='pending'))return i; |
| } |
| return -1; |
| } |
| function terminalFailureReason(){ |
| const events=activityFeedState.lastItems||[]; |
| const failed=[...events].reverse().find(event=>isFailureStatus(event.status)||String(event.step||'')==='failure'); |
| return failed?eventReason(failed):''; |
| } |
|
|
| function stepOrderIndex(step){ |
| const idx=WORKER_STEP_ORDER.indexOf(String(step||'')); |
| return idx<0?-1:idx; |
| } |
| function normalizeTimelineOverallStatus(status){ |
| const s=String(status||'').toLowerCase(); |
| if(PROGRESS_TERMINAL_STATUSES.has(s))return s; |
| return s; |
| } |
| function timelineIcon(status){ |
| status=String(status||'pending'); |
| if(status==='done'||status==='completed')return'✓'; |
| if(status==='running')return'•'; |
| if(status==='failed')return'!'; |
| if(status==='warning'||status==='warn')return'!'; |
| if(status==='fallback')return'⇄'; |
| if(status==='recovered'||status==='superseded')return'↺'; |
| if(status==='blocked'||status==='cancelled'||status==='stopped'||status==='after_fail')return'■'; |
| if(status==='skipped')return'↷'; |
| return'○'; |
| } |
| function timelineStatus(status,overallStatus=''){ |
| status=String(status||'pending').toLowerCase(); |
| const overall=normalizeTimelineOverallStatus(overallStatus); |
| 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); |
| if(status==='failed'||status==='error'||status==='failure')return'status-failed'; |
| if(status==='fallback')return'status-fallback'; |
| if(status==='warning'||status==='warn')return'status-warn'; |
| if(status==='recovered'||status==='superseded')return'status-warn'; |
| if(status==='after_fail'||status==='post_failure')return'status-after-fail'; |
| if(status==='blocked'||status==='cancelled'||status==='canceled'||status==='stopped')return'status-stopped'; |
| if(status==='done'||status==='completed'||status==='success')return'status-done'; |
| if(status==='running'||status==='waiting')return stopped?'status-stopped':'status-running'; |
| if(status==='skipped')return'status-skipped'; |
| return stopped?'status-muted':'status-pending'; |
| } |
| function timelineStepKey(x,i){ |
| return String(x.step||x.id||x.label||`step-${i}`); |
| } |
| function animateClass(el,klass,duration=900){ |
| if(!el)return; |
| el.classList.remove(klass); |
| void el.offsetWidth; |
| el.classList.add(klass); |
| window.setTimeout(()=>el.classList.remove(klass),duration); |
| } |
| function eventStableKey(event={}){ |
| return `${event.ts||''}|${event.step||''}|${event.status||''}|${event.message||''}`; |
| } |
| function mergeStableEvents(runId,items){ |
| const incoming=(items||[]).filter(Boolean); |
| if(activityFeedState.runId!==runId){ |
| activityFeedState.runId=runId; |
| activityFeedState.lastItems=[]; |
| |
| } |
| const byKey=new Map(); |
| [...activityFeedState.lastItems,...incoming].forEach(event=>{ |
| const key=eventStableKey(event); |
| if(key.trim()!=='|||')byKey.set(key,event); |
| }); |
| const merged=[...byKey.values()].sort((a,b)=>String(a.ts||'').localeCompare(String(b.ts||''))); |
| activityFeedState.lastItems=merged; |
| return merged; |
| } |
| function activeTimelineStepKey(items=[]){ |
| const active=(items||[]).find(x=>['running','stopped','failed','blocked','cancelled','canceled'].includes(String(x.status||'').toLowerCase())); |
| if(active)return String(active.step||active.id||active.label||''); |
| const done=[...(items||[])].reverse().find(x=>['done','completed','success'].includes(String(x.status||'').toLowerCase())); |
| return done?String(done.step||done.id||done.label||''):''; |
| } |
| function activeTimelineItem(items=[]){ |
| const key=activeTimelineStepKey(items); |
| return (items||[]).find(x=>String(x.step||x.id||x.label||'')===key)||null; |
| } |
| function friendlyTerminalEventMessage(event={}){ |
| const step=String(event.step||'').toLowerCase(); |
| const status=String(event.status||'').toLowerCase(); |
| const msg=String(event.message||'').trim(); |
| if(step==='api_validation'&&(status==='waiting'||status==='started')&&(msg.toLowerCase().includes('not ready')||msg.toLowerCase().includes('waiting'))){ |
| return 'Waiting for Space startup'; |
| } |
| if(step==='anonymous_eval'&&status==='success'){ |
| return 'Wrote local eval record'; |
| } |
| if(step==='done'&&(status==='full_inference_candidate_health_passed'||status==='health_only'||status==='partial_validation')){ |
| return 'Universal model-card builder completed · Partial validation'; |
| } |
| if(step==='inference_gate'&&(status==='full_inference_candidate_health_passed'||status==='health_only'||status==='partial_validation')){ |
| return 'Partial validation · health passed, generation smoke failed'; |
| } |
| return msg||event.step||'Waiting for worker events.'; |
| } |
| function eventMessage(event){ |
| return friendlyTerminalEventMessage(event||{}); |
| } |
|
|
| function hasCanonicalTimelineModel(model){ |
| return Boolean(model&&model.schema_version==='run_timeline_model.v1'&&Array.isArray(model.phases)); |
| } |
| function normalizeTerminalUiStatus(value){ |
| const s=String(value||'').trim().toLowerCase(); |
| if(!s)return''; |
| if(PROGRESS_BLOCKING_TERMINAL_STATUSES.has(s))return s==='cancelled'||s==='canceled'?'stopped':s; |
| if(['success','succeeded','done','completed','passed','repair_success'].includes(s))return'full_inference_success'; |
| if(['cancelled','canceled'].includes(s))return'stopped'; |
| if(['partial','health_only','full_inference_candidate_health_passed','completed_with_warnings'].includes(s))return'partial_validation'; |
| if(PROGRESS_TERMINAL_STATUSES.has(s))return s; |
| return''; |
| } |
| function sourceHasBlockingTerminal(src={}){ |
| const keys=['status','final_status','display_status','effective_status','effective_verdict','verdict','result_status','validation_status','ui_status','gate_status']; |
| return keys.some(k=>PROGRESS_BLOCKING_TERMINAL_STATUSES.has(String(src?.[k]||'').trim().toLowerCase())); |
| } |
| function authoritativeTerminalUiStatus(payload={}){ |
| const view=payload.view||{}; |
| const summary=payload.summary||{}; |
| const highPriority=[payload.final_status_reconciliation,view.final_status_reconciliation,summary,view.summary,view.effective_run_status,payload.effective_run_status,summary.effective_run_status].filter(x=>x&&typeof x==='object'); |
| const lowPriority=[view.timeline_model?.progress,payload.timeline_model?.progress,payload.inference_gate,payload.generation_smoke].filter(x=>x&&typeof x==='object'); |
| const keys=['status','final_status','display_status','effective_status','effective_verdict','verdict','result_status','validation_status']; |
| let sawBlocking=false; |
| for(const src of highPriority){ |
| if(sourceHasBlockingTerminal(src))sawBlocking=true; |
| for(const key of keys){ |
| const normalized=normalizeTerminalUiStatus(src[key]); |
| if(normalized){ |
| if(PROGRESS_BLOCKING_TERMINAL_STATUSES.has(normalized))return normalized; |
| if(normalized==='full_inference_success'&&sawBlocking)continue; |
| return normalized; |
| } |
| } |
| } |
| for(const src of lowPriority){ |
| if(sourceHasBlockingTerminal(src))return normalizeTerminalUiStatus(src.status||src.final_status||src.display_status||src.effective_status||src.verdict)||'technical_blocker'; |
| for(const key of keys){ |
| const normalized=normalizeTerminalUiStatus(src[key]); |
| if(normalized){ |
| if(PROGRESS_BLOCKING_TERMINAL_STATUSES.has(normalized))return normalized; |
| if(normalized==='full_inference_success'&&sawBlocking)continue; |
| return normalized; |
| } |
| } |
| if(!sawBlocking&&(src.ok===true||src.generation_smoke_passed===true||src.implementation_signals?.generation_smoke_passed===true))return'full_inference_success'; |
| } |
| return''; |
| } |
| function canonicalRunUiState(payload={}){ |
| const view=payload.view||{}; |
| const summary=payload.summary||{}; |
| const statePayload=payload.state||{}; |
| const header=view.header||{}; |
| const statusModel=view.status_model||payload.status_model||{}; |
| const modelProgress=(view.timeline_model&&view.timeline_model.progress)||payload.timeline_model?.progress||{}; |
| const effectiveRunStatus=view.effective_run_status||payload.effective_run_status||summary.effective_run_status||{}; |
| const authoritativeStatus=authoritativeTerminalUiStatus(payload); |
| const effectiveStatusLower=String(authoritativeStatus||effectiveRunStatus.display_status||effectiveRunStatus.effective_status||'').toLowerCase(); |
| const effectiveTerminalStatuses=new Set(['full_inference_success','partial_validation','manual_hardware_required','technical_blocker','technical_blocker_boot_only','failed','failure','error','stale','stopped','cancelled','canceled','auth_refresh_required','validated_after_space_test','validated_after_manual_space_test','recovered_by_space_test','recovered_by_manual_validation','manual_validated','manual_validation_passed']); |
| const terminal=Boolean(authoritativeStatus||modelProgress.terminal===true||statusModel.is_terminal===true||effectiveTerminalStatuses.has(effectiveStatusLower)); |
| const statusCandidates=[ |
| authoritativeStatus, |
| terminal?effectiveRunStatus.display_status:'', |
| terminal?modelProgress.verdict:'', |
| terminal?statusModel.effective_status:'', |
| terminal?statusModel.verdict:'', |
| terminal?statusModel.status:'', |
| terminal?header.display_status:'', |
| terminal?header.status:'', |
| payload.effective_status,summary.effective_status, |
| payload.validation_status,payload.result_status,summary.validation_status,summary.result_status, |
| payload.inference_gate?.status,summary.gate_status, |
| header.status,header.raw_status, |
| payload.status,summary.status,statePayload.status, |
| ]; |
| let status=statusCandidates.map(x=>String(x||'').trim()).find(Boolean)||'running'; |
| const lower=status.toLowerCase(); |
| const visual=String(modelProgress.visual_status||statusModel.visual_status||payload.visual_status||'').toLowerCase() || |
| (['partial','partial_validation','full_inference_candidate_health_passed','health_only','manual_hardware_required','completed_with_warnings','demo_usable_full_promise_not_verified','interactive_app_available_smoke_failed','manual_test_required_smoke_failed'].includes(lower)?'warn': |
| (lower.includes('fail')||lower.includes('error')||lower.includes('block')?'error': |
| (lower.includes('success')||lower==='succeeded'||lower==='done'?'success':lower))); |
| let label=effectiveRunStatus.display_label||modelProgress.label||header.display_label||header.status_label||badgeLabel(status); |
| if(terminal&&String(label||'').toLowerCase()==='running')label=badgeLabel(status); |
| return {status, statusLower:lower, label, visual_status:visual, isTerminal:terminal||PROGRESS_TERMINAL_STATUSES.has(lower)}; |
| } |
| function visibleCanonicalTimelinePhases(model={}){ |
| const phases=Array.isArray(model.phases)?model.phases:[]; |
| const terminal=model.progress?.terminal===true||PROGRESS_TERMINAL_STATUSES.has(String(model.progress?.verdict||'').toLowerCase()); |
| |
| |
| |
| return phases.filter(p=>terminal||String(p.id||'').toLowerCase()!=='done').map(p=>{ |
| if(String(p.id||'').toLowerCase()==='hardware')return {...p,label:'GPU'}; |
| return p; |
| }); |
| } |
| function compactPhaseSublabel(phase={}){ |
| const id=String(phase.id||'').toLowerCase(); |
| const s=String(phase.status||'pending').toLowerCase(); |
| if(s==='complete'||s==='done'||s==='success')return id==='archive'?'Published':'Done'; |
| if(s==='warning'||s==='warn')return id==='hardware'?'Fallback':'Needs attention'; |
| if(s==='running'||s==='waiting')return 'Running'; |
| if(s==='failed'||s==='error')return 'Failed'; |
| if(s==='stopped'||s==='cancelled'||s==='blocked')return 'Stopped'; |
| if(s==='not_needed'||s==='skipped')return 'Not needed'; |
| return 'Pending'; |
| } |
| function compactTimelineSublabel(status='',groupId=''){ |
| const s=String(status||'pending').toLowerCase(); |
| const id=String(groupId||'').toLowerCase(); |
| if(s==='done'||s==='complete'||s==='success')return id==='archive'?'Published':'Done'; |
| if(s==='fallback')return 'Fallback'; |
| if(s==='recovered')return 'Recovered'; |
| if(s==='warning'||s==='warn')return 'Needs attention'; |
| if(s==='running'||s==='waiting')return 'Running'; |
| if(s==='failed'||s==='error')return 'Failed'; |
| if(s==='stopped'||s==='cancelled'||s==='blocked')return 'Stopped'; |
| if(s==='skipped'||s==='not_needed')return 'Not needed'; |
| return 'Pending'; |
| } |
| function hideEventReason(item={}){ |
| const step=String(item.step||'').toLowerCase(); |
| const status=String(item.status||'').toLowerCase(); |
| if(step==='api_validation'&&(status==='waiting'||status==='started'||status==='running'))return true; |
| return false; |
| } |
| function canonicalPhaseToTimelineItem(phase={}){ |
| const raw=String(phase.status||'pending').toLowerCase(); |
| let status=raw; |
| if(raw==='complete')status='done'; |
| if(raw==='warning')status='warning'; |
| if(raw==='failed')status='failed'; |
| if(raw==='stopped')status='stopped'; |
| if(raw==='not_needed'||raw==='skipped')status='skipped'; |
| const details=Array.isArray(phase.details)?phase.details:[]; |
| return { |
| id:phase.id||phase.label, |
| step:phase.id||phase.label, |
| label:phase.label||phase.id||'Phase', |
| status, |
| summary:phase.summary||'', |
| sublabel:compactPhaseSublabel(phase), |
| details, |
| percent:status==='done'||status==='warning'||status==='skipped'?100:(status==='running'?50:0), |
| ring_progress:status==='done'||status==='warning'||status==='skipped'?100:(status==='running'?50:0), |
| }; |
| } |
| function timelinePhaseById(phases=[],id=''){ |
| return phases.find(p=>String(p.id||'')===id)||null; |
| } |
| function firstPhaseWithStatus(phases=[],statuses=[]){ |
| const wanted=new Set(statuses.map(s=>String(s||'').toLowerCase())); |
| return phases.find(p=>wanted.has(String(p.status||'').toLowerCase()))||null; |
| } |
| function firstPhaseWithIdsAndStatus(phases=[],ids=[],statuses=[]){ |
| const wantedIds=new Set(ids); |
| const wantedStatuses=new Set(statuses.map(s=>String(s||'').toLowerCase())); |
| return phases.find(p=>wantedIds.has(String(p.id||''))&&wantedStatuses.has(String(p.status||'').toLowerCase()))||null; |
| } |
| function timelineModelActivePhase(model={}){ |
| const phases=visibleCanonicalTimelinePhases(model); |
| if(!phases.length)return null; |
| const progress=model.progress||{}; |
| const visual=String(progress.visual_status||'').toLowerCase(); |
| const verdict=String(progress.verdict||'').toLowerCase(); |
| const terminal=progress.terminal===true||['success','warn','warning','error','stopped'].includes(visual)||PROGRESS_TERMINAL_STATUSES.has(verdict); |
| const failed=firstPhaseWithStatus(phases,['failed','stopped']); |
| if(failed)return failed; |
| const attention=firstPhaseWithIdsAndStatus(phases,['live_validation','archive','recovery','done'],['warning','failed'])||firstPhaseWithStatus(phases,['warning']); |
| if(terminal&&attention)return attention; |
| if(terminal&&['partial','partial_validation','full_inference_candidate_health_passed','health_only','completed_with_warnings','demo_usable_full_promise_not_verified','interactive_app_available_smoke_failed','manual_test_required_smoke_failed'].includes(verdict)){ |
| return firstPhaseWithIdsAndStatus(phases,['live_validation','archive','done'],['complete','warning'])||attention||[...phases].reverse().find(p=>String(p.status||'').toLowerCase()!=='pending')||phases[0]; |
| } |
| if(terminal&&visual==='success'){ |
| return firstPhaseWithIdsAndStatus(phases,['live_validation'],['complete','warning'])|| |
| firstPhaseWithIdsAndStatus(phases,['archive'],['complete','warning'])|| |
| timelinePhaseById(phases,'done')|| |
| [...phases].reverse().find(p=>String(p.status||'').toLowerCase()!=='pending')||phases[0]; |
| } |
| if(terminal&&(visual==='warn'||visual==='warning')){ |
| return attention||firstPhaseWithIdsAndStatus(phases,['live_validation','archive','done'],['complete'])|| |
| [...phases].reverse().find(p=>String(p.status||'').toLowerCase()!=='pending')||phases[0]; |
| } |
| const recoveryRunning=firstPhaseWithIdsAndStatus(phases,['recovery'],['running']); |
| if(recoveryRunning)return recoveryRunning; |
| const liveRunning=firstPhaseWithIdsAndStatus(phases,['live_validation'],['running']); |
| if(liveRunning)return liveRunning; |
| const agentRunning=firstPhaseWithIdsAndStatus(phases,['agent'],['running']); |
| if(agentRunning)return agentRunning; |
| const running=firstPhaseWithStatus(phases,['running']); |
| if(running)return running; |
| const actionableWarning=phases.find(p=>['live_validation','recovery','archive','done'].includes(String(p.id||''))&&String(p.status||'').toLowerCase()==='warning'); |
| if(actionableWarning)return actionableWarning; |
| return [...phases].reverse().find(p=>String(p.status||'').toLowerCase()!=='pending')||phases[0]||null; |
| } |
| function renderTimelineModelWarnings(model={}){ |
| const note=$('timelineTraceNote'); |
| if(!note)return; |
| const warnings=Array.isArray(model.warnings)?model.warnings:[]; |
| if(!warnings.length){note.hidden=true;note.innerHTML='';note.className='timeline-trace-note';return;} |
| const rows=warnings.slice(0,4).map(w=>{ |
| const code=String(w.code||''); |
| const label=escapeHtml(code==='pi_model_changed'?'Pi assistant mismatch':code==='hardware_fallback_used'?'GPU fallback':(w.label||'Run note')); |
| const detail=escapeHtml(w.detail||''); |
| return `<li><strong>${label}</strong>${detail?`<span>${detail}</span>`:''}</li>`; |
| }).join(''); |
| note.hidden=false; |
| note.className='timeline-trace-note fallback canonical-warning run-notes'; |
| note.innerHTML=`<strong><span>ℹ</span>Run notes</strong><ul>${rows}</ul>`; |
| } |
| function timelineDetailStatusLabel(status=''){ |
| const s=String(status||'').toLowerCase(); |
| if(s==='complete'||s==='done'||s==='success')return'Complete'; |
| if(s==='selected')return'Selected'; |
| if(s==='uploaded')return'Uploaded'; |
| if(s==='ready')return'Ready'; |
| if(s==='verified')return'Verified'; |
| if(s==='fallback')return'Fallback'; |
| if(s==='building')return'Building'; |
| if(s==='queued')return'Queued'; |
| if(s==='blocked')return'Blocked'; |
| if(s==='warning'||s==='warn')return'Needs attention'; |
| if(s==='running'||s==='waiting')return'In progress'; |
| if(s==='not_needed'||s==='skipped')return'Not needed'; |
| if(s==='failed'||s==='error')return'Failed'; |
| if(s==='stopped'||s==='cancelled'||s==='blocked')return'Stopped'; |
| return badgeLabel(s||'pending'); |
| } |
| function timelinePhaseDetailRows(phase={}){ |
| const details=Array.isArray(phase.details)?phase.details.filter(Boolean):[]; |
| if(details.length){ |
| return details.slice(0,6).map(d=>{ |
| const label=escapeHtml(d.label||d.title||'Detail'); |
| const value=escapeHtml(d.value||d.detail||d.summary||d.status||''); |
| const status=String(d.status||'').toLowerCase(); |
| const cls=status?` ${statusClass(status)}`:''; |
| return `<li class="timeline-detail-row${cls}"><span>${label}</span>${value?`<strong>${value}</strong>`:''}</li>`; |
| }).join(''); |
| } |
| const summary=String(phase.summary||'').trim(); |
| return summary?`<li class="timeline-detail-row neutral"><span>Summary</span><strong>${escapeHtml(summary)}</strong></li>`:''; |
| } |
| function renderTimelineModelDetails(model={},activePhase=null){ |
| const root=$('timelinePhaseDetails'); |
| if(!root)return; |
| if(!hasCanonicalTimelineModel(model)){root.hidden=true;root.innerHTML='';root.className='timeline-phase-details';return;} |
| const phase=activePhase||timelineModelActivePhase(model)||{}; |
| if(!phase||!(phase.id||phase.label)){root.hidden=true;root.innerHTML='';return;} |
| const status=String(phase.status||'pending').toLowerCase(); |
| const tone=timelineStatus(status).replace(/^status-/,''); |
| const warnings=Array.isArray(model.warnings)?model.warnings:[]; |
| const details=Array.isArray(phase.details)?phase.details:[]; |
| const phaseId=String(phase.id||''); |
| const isRecovery=String(phase.id||'')==='recovery'; |
| const actionable=status.includes('fail')||status.includes('error')||status.includes('warn')||status.includes('manual')||isRecovery||warnings.length>0||details.some(d=>{const st=String(d?.status||'').toLowerCase();return st.includes('fail')||st.includes('warn')||st.includes('manual')||st==='info';}); |
| if(!actionable){root.hidden=true;root.innerHTML='';root.className='timeline-phase-details';return;} |
| let rows=timelinePhaseDetailRows(phase); |
| let summary=phase.summary||''; |
| if(String(phase.id||'')==='recovery'){ |
| |
| |
| |
| summary=summary||'Recovery status is available below.'; |
| |
| rows=''; |
| } |
| root.hidden=false; |
| root.className=`timeline-phase-details ${tone}${isRecovery?' recovery-compact':''}`; |
| const compactPointer=isRecovery?`<p class="timeline-detail-pointer">See Agent recovery below for diagnosis, patch and blocker details.</p>`:''; |
| root.innerHTML=`<div class="timeline-detail-head"><span>${escapeHtml(isRecovery?'Current focus':(phase.label||'Phase'))}</span><strong>${escapeHtml(timelineDetailStatusLabel(status))}</strong></div>${summary?`<p>${escapeHtml(summary)}</p>`:''}${compactPointer}${rows?`<ul>${rows}</ul>`:''}`; |
| } |
| function renderTimelineModel(model={},overallStatus=''){ |
| const root=$('timeline'); |
| if(!root)return false; |
| if(!hasCanonicalTimelineModel(model))return false; |
| if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{if(root.dataset.autoScrolling==='1')return;root.dataset.userScrolled='1'},{passive:true});} |
| const list=visibleCanonicalTimelinePhases(model).map(canonicalPhaseToTimelineItem); |
| const wideTimeline=typeof window==='undefined'||!window.matchMedia||window.matchMedia('(min-width: 521px)').matches; |
| root.style.gridTemplateColumns=wideTimeline&&list.length?`repeat(${list.length}, minmax(0, 1fr))`:''; |
| root.style.setProperty('--timeline-phase-count', String(Math.max(1,list.length||8))); |
| root.classList.add('timeline-canonical'); |
| root.classList.remove('trace-failed','trace-recovered','trace-fallback'); |
| renderTimelineModelWarnings(model); |
| const active=timelineModelActivePhase(model)||{}; |
| renderTimelineModelDetails(model,active); |
| const currentKey=String(active.id||active.label||''); |
| const signature=list.map((x,i)=>`${timelineStepKey(x,i)}:${x.status}:${x.label}:${x.summary}:${overallStatus}`).join('|'); |
| if(root.dataset.signature===signature){ |
| if(root.dataset.visibleStep!==currentKey&&root.dataset.userScrolled!=='1')ensureTimelineStepVisible(currentKey); |
| return true; |
| } |
| const previousKey=root.dataset.currentStep||''; |
| if(previousKey!==currentKey)root.dataset.userScrolled=''; |
| root.dataset.signature=signature; |
| root.dataset.currentStep=currentKey||''; |
| const nextState={}; |
| root.innerHTML=list.map((x,i)=>{ |
| const key=timelineStepKey(x,i); |
| const st=timelineStatus(x.status,overallStatus); |
| const prev=timelineRenderState[key]; |
| const changed=prev&&prev!==st; |
| nextState[key]=st; |
| const label=st.replace(/^status-/,''); |
| const summary=x.summary||label; |
| const sublabel=x.sublabel||label; |
| const details=(x.details||[]).map(d=>d&&d.label).filter(Boolean); |
| const detailTitle=details.length?details.join(' · '):summary; |
| const title=` title="${escapeHtml([x.label,summary,detailTitle].filter(Boolean).join(' · '))}"`; |
| const aria=`${x.label}: ${label}${summary?`, ${summary}`:''}`; |
| 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(sublabel)}</small></div></div>`; |
| }).join(''); |
| Object.keys(timelineRenderState).forEach(k=>delete timelineRenderState[k]); |
| Object.assign(timelineRenderState,nextState); |
| root.querySelectorAll('.step.status-changed').forEach(step=>{animateClass(step,'status-flash',1000);animateClass(step.querySelector('.step-dot'),'dot-pop',700);}); |
| 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);} |
| return true; |
| } |
| function timelineStepBounds(root,target){ |
| const rootBox=root.getBoundingClientRect?root.getBoundingClientRect():{left:0,right:root.clientWidth,width:root.clientWidth}; |
| const targetBox=target.getBoundingClientRect?target.getBoundingClientRect():{left:target.offsetLeft-root.scrollLeft,right:target.offsetLeft-root.scrollLeft+target.offsetWidth,width:target.offsetWidth}; |
| return { |
| rootLeft:rootBox.left, |
| rootRight:rootBox.right, |
| targetLeft:targetBox.left, |
| targetRight:targetBox.right, |
| targetWidth:targetBox.width||target.offsetWidth||0, |
| }; |
| } |
| function ensureTimelineStepVisible(stepKey,opts={}){ |
| const early=new Set(['bootstrap','dependencies','auth_model']); |
| const root=$('timeline'); |
| if(!root||!stepKey)return; |
| const safeStep=(typeof CSS!=='undefined'&&CSS.escape)?CSS.escape(stepKey):String(stepKey).replace(/"/g,'\"'); |
| const target=root.querySelector(`[data-step="${safeStep}"]`); |
| if(!target)return; |
| const margin=28; |
| |
| const behavior='auto'; |
| const visibleLeft=root.scrollLeft; |
| const visibleRight=visibleLeft+root.clientWidth; |
| const targetLeft=target.offsetLeft; |
| const targetRight=targetLeft+target.offsetWidth; |
| const fullyVisible=targetLeft>=visibleLeft+margin&&targetRight<=visibleRight-margin; |
| if(fullyVisible){root.dataset.visibleStep=stepKey;return;} |
| if(early.has(stepKey)&&root.scrollLeft<=1&&targetLeft>=0&&targetRight<=root.clientWidth){root.dataset.visibleStep=stepKey;return;} |
| let nextLeft=visibleLeft; |
| if(targetLeft<visibleLeft+margin){ |
| nextLeft=targetLeft-margin; |
| }else if(targetRight>visibleRight-margin){ |
| nextLeft=targetRight-root.clientWidth+margin; |
| } |
| const maxScroll=Math.max(0,root.scrollWidth-root.clientWidth); |
| nextLeft=Math.max(0,Math.min(maxScroll,Math.round(nextLeft))); |
| if(opts.force&&target.offsetWidth<root.clientWidth&&targetRight>visibleLeft&&targetLeft<visibleRight){ |
| |
| const center=targetLeft+(target.offsetWidth/2); |
| if(center>visibleRight-margin*2)nextLeft=Math.min(maxScroll,Math.round(targetRight-root.clientWidth+margin*2)); |
| if(center<visibleLeft+margin*2)nextLeft=Math.max(0,Math.round(targetLeft-margin*2)); |
| } |
| if(Math.abs(nextLeft-root.scrollLeft)>1){ |
| const behavior=opts.instant?'auto':'smooth'; |
| try{root.scrollTo({left:nextLeft,behavior});} |
| catch(_){root.scrollLeft=nextLeft;} |
| } |
| root.dataset.visibleStep=stepKey; |
| } |
| function hasSpecificTimelineProblem(items=[],overallStatus=''){ |
| const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item])); |
| const overall=visualStatusClass(overallStatus); |
| return COMPACT_TIMELINE_GROUPS.some(group=>{ |
| if(group.id==='failure')return false; |
| if(group.id==='repair')return group.steps.some(step=>byStep.has(step))&&overall==='error'; |
| return group.steps.some(step=>isFailureStatus(byStep.get(step)?.status))||failedEventInfoForGroup(group); |
| }); |
| } |
| function visibleCompactTimelineGroups(items=[],overallStatus=''){ |
| const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item])); |
| const overall=visualStatusClass(overallStatus); |
| const specificProblem=hasSpecificTimelineProblem(items,overallStatus); |
| return COMPACT_TIMELINE_GROUPS.filter(group=>{ |
| const hasItem=group.steps.some(step=>byStep.has(step)); |
| if(group.id==='repair')return hasItem; |
| |
| |
| |
| if(group.id==='failure')return hasItem&&!specificProblem; |
| return true; |
| }); |
| } |
| function compactTimelineItems(items=[],overallStatus=''){ |
| const byStep=new Map((items||[]).map(item=>[String(item.step||item.id||item.label||''),item])); |
| const terminalRaw=new Set(['failed','blocked','stopped','cancelled','canceled','recovered','fallback','after_fail','post_failure']); |
| const overall=visualStatusClass(overallStatus); |
| const terminalSuccess=overall==='success'||['success','succeeded','done','completed','full_inference_success','repair_success'].includes(String(overallStatus||'').toLowerCase()); |
| let firstFailedIdx=firstFailedGroupIndex(overallStatus); |
| const visibleGroups=visibleCompactTimelineGroups(items,overallStatus); |
| const terminalAnchorIdx=firstFailedIdx>=0?firstFailedIdx:terminalFailureAnchorGroupIndex(items,overallStatus); |
| if(overall==='error'&&firstFailedIdx<0)firstFailedIdx=terminalAnchorIdx; |
| return visibleGroups.map((group,groupIndex)=>{ |
| const groupItems=group.steps.map(step=>byStep.get(step)).filter(Boolean); |
| const explicitItems=groupItems.filter(item=>String(item.step||item.id||item.label||'')); |
| const doneCount=groupItems.filter(item=>['done','completed','success'].includes(String(item.status||'').toLowerCase())).length; |
| const active=groupItems.find(item=>['running','waiting','stopped','failed','blocked','cancelled','canceled'].includes(String(item.status||'').toLowerCase())); |
| const itemFailure=failedItemInfoForGroup(group,byStep); |
| const historicalFailure=failedTimelineStatusForGroup(group,overallStatus)||(itemFailure?(groupHasHardwareFallback(group)?'fallback':(overall==='error'?'failed':'recovered')):''); |
| let status='pending'; |
| if(historicalFailure)status=historicalFailure; |
| else if(overall==='error'&&terminalAnchorIdx>=0&&groupIndex>terminalAnchorIdx)status='pending'; |
| else if(overall==='error'&&terminalAnchorIdx>=0&&groupIndex===terminalAnchorIdx)status='failed'; |
| else if(active)status=String(active.status||'running').toLowerCase(); |
| else if(groupItems.length&&doneCount>=group.steps.length)status='done'; |
| else if(doneCount>0)status=terminalSuccess?'done':'running'; |
| if(overall==='error'&&status==='done'&&group.steps.includes('failure'))status='failed'; |
| const failedInfo=failedEventInfoForGroup(group)||itemFailure; |
| const reason=historicalFailure==='fallback'?fallbackReasonForGroup(group):(status==='failed'?(failureReasonForGroup(group)||terminalFailureReason()):(historicalFailure?failureReasonForGroup(group):'')); |
| const activeSubstep=historicalFailure==='fallback'?'fallback GPU':failedInfo&&historicalFailure?(failedInfo.event.step||failedInfo.event.message||'failed'):active?.step||active?.id||active?.label||groupItems[groupItems.length-1]?.step||''; |
| const activeIndex=activeSubstep?Math.max(0,group.steps.indexOf(activeSubstep)):-1; |
| let percent=0; |
| if(status==='done')percent=100; |
| else if(status==='pending')percent=0; |
| else if(status==='after_fail')percent=doneCount?Math.round((doneCount/group.steps.length)*100):0; |
| else if(activeIndex>=0)percent=Math.round(((activeIndex+0.45)/group.steps.length)*100); |
| else if(doneCount>0)percent=Math.round((doneCount/group.steps.length)*100); |
| if(terminalRaw.has(status))percent=Math.max(percent,doneCount?Math.round((doneCount/group.steps.length)*100):8); |
| const clampedPercent=Math.max(0,Math.min(100,percent)); |
| const substepLabel=workerStepDisplayLabel(activeSubstep); |
| const progressDone=status==='done'?group.steps.length:Math.min(group.steps.length,Math.max(doneCount,clampedPercent>=100?group.steps.length:Math.ceil((clampedPercent/100)*group.steps.length))); |
| const showProgressLabel=group.steps.length>1&&(groupItems.length>0||status!=='pending'); |
| return { |
| step:group.id, |
| id:group.id, |
| label:group.display_label||group.label, |
| status, |
| full_steps:group.steps, |
| done_count:doneCount, |
| total_count:showProgressLabel?group.steps.length:0, |
| percent:clampedPercent, |
| ring_progress:clampedPercent, |
| active_substep:activeSubstep, |
| active_substep_label:substepLabel, |
| grouped:group.steps.length>1, |
| progress_label:showProgressLabel?`${progressDone}/${group.steps.length}`:'', |
| reason |
| }; |
| }); |
| } |
| function currentCompactGroupKey(fullItems=[],overallStatus=''){ |
| const problem=firstProblemGroupKey(fullItems,overallStatus); |
| if(problem)return problem; |
| const activeKey=activeTimelineStepKey(fullItems); |
| const group=COMPACT_TIMELINE_GROUPS.find(g=>g.steps.includes(activeKey)); |
| return group?.id||activeKey; |
| } |
| function timelineTraceModel(list=[],overallStatus=''){ |
| const overall=visualStatusClass(overallStatus); |
| const failedIndex=list.findIndex(item=>String(item.status||'').toLowerCase()==='failed'); |
| const fallbackIndex=list.findIndex(item=>String(item.status||'').toLowerCase()==='fallback'); |
| const recoveredIndex=list.findIndex(item=>String(item.status||'').toLowerCase()==='recovered'); |
| if(failedIndex>=0){ |
| return {mode:'failed',index:failedIndex,key:list[failedIndex]?.step||'',label:list[failedIndex]?.label||'failed step',reason:reasonForTimelineItem(list[failedIndex]),message:`Failed at ${list[failedIndex]?.label||'this stage'}. Later steps are post-failure cleanup or reporting, not a successful build path.`}; |
| } |
| if(fallbackIndex>=0){ |
| return {mode:'fallback',index:fallbackIndex,key:list[fallbackIndex]?.step||'',label:list[fallbackIndex]?.label||'hardware fallback',reason:reasonForTimelineItem(list[fallbackIndex]),message:`ZeroGPU was not used for this run. The factory switched to the fallback GPU and continued successfully.`}; |
| } |
| if(recoveredIndex>=0){ |
| return {mode:'recovered',index:recoveredIndex,key:list[recoveredIndex]?.step||'',label:list[recoveredIndex]?.label||'recovered step',reason:reasonForTimelineItem(list[recoveredIndex]),message:`Recovered after an incident at ${list[recoveredIndex]?.label||'this stage'}. The trace continues after recovery.`}; |
| } |
| return {mode:'clean',index:-1,key:'',label:'',message:''}; |
| } |
| function renderTimelineTraceNote(trace){ |
| const note=$('timelineTraceNote'); |
| if(!note)return; |
| if(!trace||trace.mode==='clean'){ |
| note.hidden=true; |
| note.className='timeline-trace-note'; |
| note.innerHTML=''; |
| return; |
| } |
| note.hidden=false; |
| note.className=`timeline-trace-note ${trace.mode}`; |
| const icon=trace.mode==='failed'?'✕':(trace.mode==='fallback'?'⇄':'↺'); |
| const title=trace.mode==='failed'?'Failure trace':(trace.mode==='fallback'?'Hardware fallback':'Recovered incident'); |
| const reason=trace.reason?`<p class="trace-reason"><b>Reason:</b> ${escapeHtml(trace.reason)}</p>`:''; |
| note.innerHTML=`<strong><span>${icon}</span>${escapeHtml(title)}</strong><p>${escapeHtml(trace.message)}</p>${reason}`; |
| } |
| function traceClassForStep(trace,index){ |
| if(!trace||trace.mode==='clean')return''; |
| if(index===trace.index)return trace.mode==='failed'?'trace-breakpoint':(trace.mode==='fallback'?'trace-fallback':'trace-recovered'); |
| if(trace.mode==='failed'&&index>trace.index)return'trace-post-failure'; |
| return''; |
| } |
| function renderTimelinePlaceholder(message='Waiting for canonical run timeline…'){ |
| const root=$('timeline'); |
| if(!root)return; |
| root.dataset.signature='canonical-placeholder'; |
| root.classList.remove('timeline-canonical','trace-failed','trace-recovered','trace-fallback'); |
| root.classList.add('timeline-placeholder'); |
| root.style.gridTemplateColumns=''; |
| root.innerHTML=`<div class="timeline-skeleton">${escapeHtml(message)}</div>`; |
| 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';} |
| } |
| function shouldSuppressLegacyTimeline(items=[]){ |
| const list=Array.isArray(items)?items:[]; |
| if(!list.length)return true; |
| const labels=list.map(x=>String(x.label||'').toLowerCase()); |
| const legacyNeedles=['bootstrap','dependencies','pi install','pi config','requirements sanitize','generation smoke','inference gate']; |
| const legacyHits=legacyNeedles.filter(n=>labels.includes(n)).length; |
| return legacyHits>=4&&list.length>=12; |
| } |
| function renderTimeline(items,overallStatus=''){ |
| const root=$('timeline'); |
| if(!root)return; |
| if(shouldSuppressLegacyTimeline(items)){renderTimelinePlaceholder('Waiting for canonical run timeline…');return;} |
| if(!root.dataset.scrollBound){root.dataset.scrollBound='1';root.addEventListener('scroll',()=>{if(root.dataset.autoScrolling==='1')return;root.dataset.userScrolled='1'},{passive:true});} |
| root.classList.remove('timeline-canonical','timeline-placeholder'); |
| const canonicalDetails=$('timelinePhaseDetails'); |
| if(canonicalDetails){canonicalDetails.hidden=true;canonicalDetails.innerHTML='';canonicalDetails.className='timeline-phase-details';} |
| const fullList=items||[]; |
| const list=compactTimelineItems(fullList,overallStatus); |
| |
| |
| |
| |
| |
| const wideTimeline=typeof window==='undefined'||!window.matchMedia||window.matchMedia('(min-width: 521px)').matches; |
| root.style.gridTemplateColumns=wideTimeline&&list.length?`repeat(${list.length}, minmax(96px, 1fr))`:''; |
| const trace=timelineTraceModel(list,overallStatus); |
| renderTimelineTraceNote(trace); |
| root.classList.toggle('trace-failed',trace.mode==='failed'); |
| root.classList.toggle('trace-recovered',trace.mode==='recovered'); |
| root.classList.toggle('trace-fallback',trace.mode==='fallback'); |
| const currentKey=trace.key||currentCompactGroupKey(fullList,overallStatus); |
| const signature=list.map((x,i)=>`${timelineStepKey(x,i)}:${timelineStatus(x.status,overallStatus)}:${x.label||''}:${overallStatus||''}:${trace.mode}:${trace.index}`).join('|'); |
| |
| if(root.dataset.signature===signature){ |
| if(root.dataset.visibleStep!==currentKey&&root.dataset.userScrolled!=='1')ensureTimelineStepVisible(currentKey); |
| return; |
| } |
| const previousKey=root.dataset.currentStep||''; |
| if(previousKey!==currentKey)root.dataset.userScrolled=''; |
| root.dataset.signature=signature; |
| root.dataset.currentStep=currentKey||''; |
| const nextState={}; |
| root.innerHTML=list.map((x,i)=>{ |
| const key=timelineStepKey(x,i); |
| const st=timelineStatus(x.status,overallStatus); |
| const rawStatus=String(x.status||'pending').toLowerCase(); |
| const prev=timelineRenderState[key]; |
| const changed=prev&&prev!==st; |
| nextState[key]=st; |
| const label=st.replace(/^status-/,''); |
| const percent=Number(x.percent||0); |
| const meta=compactTimelineSublabel(rawStatus,x.id||x.step||''); |
| const traceClass=traceClassForStep(trace,i); |
| const reason=reasonForTimelineItem(x); |
| const reasonLine=''; |
| const titleParts=[x.label||`Step ${i+1}`,meta,reason].filter(Boolean); |
| const title=` title="${escapeHtml(titleParts.join(' · '))}"`; |
| const groupBadge=x.total_count>1?`<em class="step-count" aria-label="${escapeHtml((x.progress_label||'')+' substeps')}">${escapeHtml(x.progress_label||'')}</em>`:''; |
| const aria=`${x.label||`Step ${i+1}`}: ${label}${x.total_count>1?`, ${x.done_count||0} of ${x.total_count} substeps`:''}`; |
| return `<div class="step ${st} ${traceClass}${x.grouped?' is-grouped':''}${changed?' status-changed':''}" data-step="${escapeHtml(key)}" data-status="${label}" aria-label="${escapeHtml(aria)}" style="--step-progress:${percent}%"${title}><div class="step-dot"><span>${timelineIcon(rawStatus)}</span>${groupBadge}</div><div><strong>${escapeHtml(x.label||`Step ${i+1}`)}</strong><small>${escapeHtml(meta)}</small>${reasonLine}</div></div>`; |
| }).join(''); |
| Object.keys(timelineRenderState).forEach(k=>delete timelineRenderState[k]); |
| Object.assign(timelineRenderState,nextState); |
| root.querySelectorAll('.step.status-changed').forEach(step=>{ |
| animateClass(step,'status-flash',1000); |
| const dot=step.querySelector('.step-dot'); |
| animateClass(dot,'dot-pop',700); |
| }); |
| 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);} |
| } |
| function contextualEventClass(item={},furthestIdx=-1){ |
| const raw=String(item.status||'').toLowerCase(); |
| const idx=stepOrderIndex(item.step); |
| if(isFailureStatus(raw)&&isHardwareFallbackEvent(item))return'hardware-fallback'; |
| if(isFailureStatus(raw)&&idx>=0&&furthestIdx>idx)return'warn recovered'; |
| if(isFailureStatus(raw))return'error'; |
| if(isStoppedStatus(raw))return'warn'; |
| if(isSuccessStatus(raw))return'success'; |
| if(raw.includes('running')||raw.includes('started')||raw.includes('waiting')||raw.includes('pending'))return'running'; |
| return statusClass(raw); |
| } |
| function contextualEventLabel(item={},klass=''){ |
| const raw=String(item.status||'').toLowerCase(); |
| if(klass.includes('hardware-fallback'))return`${raw||'fallback'} · fallback route`; |
| if(klass.includes('recovered'))return`${raw||'failed'} · superseded`; |
| return raw; |
| } |
| function eventIconFor(item={},klass=''){ |
| const step=String(item.step||'').toLowerCase(); |
| const status=String(item.status||'').toLowerCase(); |
| if(klass.includes('error')||status.includes('fail')||status.includes('error'))return'alert'; |
| if(klass.includes('hardware-fallback')||step.includes('hardware'))return'chip'; |
| if(step.includes('repair_decision')||step.includes('pi_diagnosis')||step.includes('failure_diagnosis'))return'search'; |
| if(step.includes('factory_rebuild'))return'rocket'; |
| if(step.includes('repair')||step.includes('blocker'))return'wand'; |
| if(step.includes('upload'))return'upload'; |
| if(step.includes('trace'))return'files'; |
| if(step.includes('report'))return'document'; |
| if(step.includes('auth'))return'shield'; |
| if(step.includes('model')||step.includes('analysis'))return'search'; |
| if(step.includes('space')||step.includes('create'))return'rocket'; |
| if(step.includes('api')||step.includes('smoke')||step.includes('inference')||step.includes('validation'))return'bolt'; |
| if(step.includes('pi_')||step.includes('pi'))return'bot'; |
| if(status.includes('running')||status.includes('started')||status.includes('waiting')||status.includes('pending'))return'clock'; |
| if(status.includes('success')||status==='done')return'check'; |
| return'circle'; |
| } |
| const EVENT_ICON_PATHS={ |
| alert:'M12 9v4m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z', |
| bolt:'M13 2 4 14h7l-1 8 10-13h-7l0-7Z', |
| bot:'M12 5V3m-7 8a5 5 0 0 1 5-5h4a5 5 0 0 1 5 5v5a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-5Zm4 1h2m-8 0h2m-2 4h4', |
| check:'m5 13 4 4L19 7', |
| chip:'M9 3v3m6-3v3M9 18v3m6-3v3M3 9h3m-3 6h3m12-6h3m-3 6h3M8 6h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2Zm2 4h4v4h-4v-4Z', |
| circle:'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z', |
| clock:'M12 8v5l3 2m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z', |
| document:'M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7l-5-5Zm0 0v5h5M8 13h8M8 17h6', |
| files:'M9 7h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2Zm0 0V5a2 2 0 0 1 2-2h4l4 4v2M11 11h4M11 15h5', |
| rocket:'M5 19c1.5-.5 2.8-1.5 4-3m-4 3c-.8.8-1.8 1.1-3 1 .1-1.2.5-2.2 1.2-3M13 7l4 4m-6 6-4-4 4-7c2-2 5-3 8-3 0 3-1 6-3 8l-5 6Z', |
| search:'M21 21l-4.35-4.35M10.5 18a7.5 7.5 0 1 1 0-15 7.5 7.5 0 0 1 0 15Z', |
| shield:'M12 3 5 6v6c0 4.4 3 7.4 7 9 4-1.6 7-4.6 7-9V6l-7-3Zm-3 9 2 2 4-5', |
| upload:'M12 16V4m0 0 4 4m-4-4-4 4M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3', |
| wand:'M4 20 20 4m-3 0 3 3m-6-1 4 4M5 6h.01M9 3h.01M3 10h.01M18 18h.01' |
| }; |
| function renderEventIcon(name){ |
| const key=EVENT_ICON_PATHS[name]?name:'circle'; |
| return `<span class="activity-icon" data-event-icon="${key}" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="${EVENT_ICON_PATHS[key]}"></path></svg></span>`; |
| } |
| |
| function eventOriginFor(item={}){ |
| const step=String(item.step||'').toLowerCase(); |
| const message=String(item.message||'').toLowerCase(); |
| if(['pi_install','pi_config','pi_run','pi_model_resolution','pi_verification','pi_diagnosis','repair_plan','repair_patch'].some(s=>step.includes(s)))return'pi'; |
| if(['bootstrap','token_context','dependencies','node'].some(s=>step.includes(s))||message.includes('worker started'))return'job'; |
| if(['create_space','create_space_hardware','hardware_strategy','upload_files'].some(s=>step.includes(s)))return'hub'; |
| if(['space_runtime','space_logs','live_wait'].some(s=>step.includes(s))||message.includes('runtime_error')||message.includes('app_starting'))return'space'; |
| if(['api_validation','generation_smoke','inference_gate','repair_validation'].some(s=>step.includes(s))||message.includes('gradio'))return'gradio'; |
| if(['failure_diagnosis','repair_decision','technical_blocker','failure','done','report_write'].some(s=>step.includes(s)))return'factory'; |
| return'factory'; |
| } |
| function renderActivityFeed(items){ |
| const root=$('activityFeed'); |
| if(!root)return; |
| const list=mergeStableEvents(String(state.runId||''),items||[]); |
| const visible=[...list].slice(-30).reverse(); |
| const signature=visible.map(item=>`${item.ts||''}:${item.step||''}:${item.status||''}:${item.message||''}`).join('|'); |
| if(root.dataset.signature===signature)return; |
| const shouldKeepLatestVisible=root.scrollTop<24||!root.dataset.signature; |
| root.dataset.signature=signature; |
| root.classList.add('show-all'); |
| if(!list.length){ |
| root.innerHTML='<div class="empty">No activity yet. Waiting for the first worker event.</div>'; |
| return; |
| } |
| const furthestIdx=timelineFurthestIndex(progressStability.timeline||[]); |
| root.innerHTML=visible.map(item=>{ |
| const klass=contextualEventClass(item,furthestIdx); |
| const label=contextualEventLabel(item,klass); |
| const reason=eventReason(item); |
| const reasonHtml=!hideEventReason(item)&&reason&&reason!==String(item.message||'').trim()?`<p class="event-reason">${escapeHtml(reason)}</p>`:''; |
| const icon=eventIconFor(item,klass); |
| const step=String(item.step||'event').replace(/_/g,' '); |
| const origin=eventOriginFor(item); |
| return `<div class="activity-row ${klass}"><time>${escapeHtml(formatTime(item.ts))}</time>${renderEventIcon(icon)}<div class="activity-copy"><strong title="${escapeHtml(item.message||'Event received')}">${escapeHtml(item.message||'Event received')}</strong><p title="${escapeHtml(step+' · '+(label||''))}"><span>${escapeHtml(step)}</span><em>${escapeHtml(label||'')}</em></p>${reasonHtml}</div><span class="event-origin ${origin}">${escapeHtml(origin)}</span></div>`; |
| }).join(''); |
| if(shouldKeepLatestVisible)root.scrollTop=0; |
| } |
|
|
| function renderDiagnostics(d={},header={}){ |
| setText('diagBuildStatus',badgeLabel(d.build_status||'pending')); |
| setText('diagBuildStep',header.current_phase_label||'Waiting for run data'); |
| const chips=$('statusChips'); |
| if(chips){ |
| const data=[['Build',d.build_status],['API',d.api_status],['Tests',d.tests_status],['Verdict',d.verdict_status]]; |
| chips.innerHTML=data.map(([k,v])=>`<span class="status-chip ${statusClass(v)}"><strong>${escapeHtml(k)}</strong><em>${escapeHtml(badgeLabel(v||'pending'))}</em></span>`).join(''); |
| } |
| const issue=$('issueCard'); |
| if(issue){ |
| const has=Boolean(d.issue&&d.issue.title); |
| issue.hidden=!has; |
| if(has){ |
| issue.className='issue-card '+statusClass(d.issue.status||d.issue.title); |
| setText('issueTitle',d.issue.title); |
| setText('issueDetail',d.issue.detail||d.issue.status||'Review logs.'); |
| } |
| } |
| const rules=$('zeroGpuChecklist'); |
| if(rules)rules.innerHTML=(d.zerogpu_rules||[]).map(r=>`<li class="${statusClass(r.status)}"><span>${escapeHtml(r.label)}</span><strong>${escapeHtml(r.status||'pending')}</strong></li>`).join(''); |
| } |
| function renderSpaceTestPreview(s={}){ |
| const policy=s.policy||s.space_test_policy||{}; |
| const metrics=s.validation_metrics||{}; |
| const status=policy.mode||s.status||'pending'; |
| const st=$('spaceTestStatus'); |
| if(st){ |
| st.textContent=policy.label||badgeLabel(status); |
| st.className='badge '+(policy.enabled?statusClass(status):'neutral'); |
| } |
| const endpointLabel=policy.requires_endpoint_discovery?'Discovery first':(s.endpoint||policy.endpoint||metrics.api_name||s.output_artifact||'—'); |
| setText('spaceTestEndpoint',endpointLabel); |
| setText('spaceTestOutput',metrics.output_artifact||s.output_artifact||'Not launched'); |
| const latency=metrics.latency_seconds||metrics.observed_latency_seconds||policy.smoke_latency_seconds||s.smoke_latency_seconds||s.latency_seconds||''; |
| if(latency)setText('spaceTestLatency',`${formatLatencySeconds(latency)} from ${metrics.source||'automatic smoke'}`); |
| setText('spaceTestVerdict',policy.message||s.verdict||'pending'); |
| } |
| function animateProgressBar(progress,status,currentStep){ |
| const fill=$('progressFill'); |
| if(!fill)return; |
| const value=Math.max(0,Math.min(100,Number(progress||0))); |
| const previous=progressMotionState.lastProgress; |
| let klass=visualStatusClass(status);if(value>=100&&klass==='neutral')klass='success'; |
| fill.style.setProperty('--progress-target',`${value}%`); |
| fill.style.width=`${value}%`; |
| fill.classList.remove('is-success','is-warn','is-error','is-running-state','is-stopped','is-neutral'); |
| fill.classList.add(`is-${klass==='running'?'running-state':klass}`); |
| const s=String(status||'').toLowerCase();fill.classList.toggle('is-running',klass==='running'||s==='pending'||s==='scheduled'||s==='waiting'); |
| if(previous!==null&&Math.round(previous)!==Math.round(value)){ |
| animateClass(fill,'progress-bump',650); |
| const pct=$('progressPercent'); |
| animateClass(pct,'value-bump',650); |
| } |
| progressMotionState.lastProgress=value; |
| progressMotionState.lastStep=currentStep||progressMotionState.lastStep; |
| } |
| function animateStatusBadge(status){ |
| const el=$('progressStatus'); |
| if(!el)return; |
| const next=String(status||'').toLowerCase(); |
| if(progressMotionState.lastStatus&&progressMotionState.lastStatus!==next){ |
| animateClass(el,'status-bump',800); |
| } |
| progressMotionState.lastStatus=next; |
| } |
|
|
| function progressProcessLabel(progress={},percent=0){ |
| const terminal=Boolean(progress.terminal); |
| const status=String(progress.process_status||'').toLowerCase(); |
| if(terminal||Number(percent)>=100)return'Completed'; |
| if(status.includes('running')||status.includes('waiting')||status.includes('pending'))return'In progress'; |
| return status?badgeLabel(status):'In progress'; |
| } |
| function progressVerdictLabel(progress={},visualStatus=''){ |
| const label=String(progress.label||'').trim(); |
| const verdict=String(progress.verdict||visualStatus||'').trim(); |
| if(label&&!(progress.terminal===true&&label.toLowerCase()==='running'))return label; |
| return verdict?badgeLabel(verdict):'Pending verdict'; |
| } |
| function renderSemanticProgressSummary(progress={},percent=0,visualStatus=''){ |
| const root=$('progressSemantics'); |
| const caption=$('progressCaptionLabel'); |
| const bar=$('progressBar'); |
| if(caption)caption.textContent=progress&&progress.verdict?'Process progress':'Overall progress'; |
| if(bar){ |
| const value=Math.max(0,Math.min(100,Number(percent||0))); |
| bar.setAttribute('aria-valuenow',String(Math.round(value))); |
| bar.setAttribute('aria-label',progress&&progress.verdict?`Process progress ${Math.round(value)} percent, result ${progressVerdictLabel(progress,visualStatus)}`:`Overall progress ${Math.round(value)} percent`); |
| } |
| if(!root)return; |
| if(!progress||!progress.verdict){root.hidden=true;root.innerHTML='';return;} |
| const process=progressProcessLabel(progress,percent); |
| const verdict=progressVerdictLabel(progress,visualStatus); |
| const tone=visualStatusClass(visualStatus||progress.visual_status||progress.verdict); |
| const subtitle=String(progress.subtitle||'').trim(); |
| root.hidden=false; |
| root.className=`progress-semantics ${tone}`; |
| root.innerHTML=`<span><strong>Process</strong> ${escapeHtml(process)}</span><span><strong>Result</strong> ${escapeHtml(verdict)}</span>${subtitle?`<em>${escapeHtml(subtitle)}</em>`:''}`; |
| } |
| function setElapsedClock(seconds,status){ |
| const s=String(status||'').toLowerCase(); |
| elapsedClock.baseSeconds=Math.max(0,Number(seconds||0)); |
| elapsedClock.baseAt=Date.now(); |
| elapsedClock.isRunning=Boolean(state.runId)&&!TERMINAL_STATUSES.has(s); |
| setText('elapsed',fmtSeconds(elapsedClock.baseSeconds)); |
| } |
| window.setInterval(()=>{ |
| if(!elapsedClock.isRunning)return; |
| const live=elapsedClock.baseSeconds+Math.floor((Date.now()-elapsedClock.baseAt)/1000); |
| setText('elapsed',fmtSeconds(live)); |
| },1000); |
|
|
| function progressIsSuccessStatus(status){ |
| const s=String(status||'').toLowerCase(); |
| return ['success','succeeded','done','full_inference_success'].includes(s); |
| } |
| function progressIsTerminalStatus(status){ |
| const s=String(status||'').toLowerCase(); |
| return PROGRESS_TERMINAL_STATUSES.has(s); |
| } |
| function timelineFurthestIndex(items){ |
| const list=items||[]; |
| let max=-1; |
| list.forEach((item,i)=>{ |
| const st=String(item.status||'pending').toLowerCase(); |
| if(!['pending','status-pending','muted','status-muted'].includes(st))max=i; |
| }); |
| return max; |
| } |
| function cloneTimeline(items){ |
| return (items||[]).map(x=>({...x})); |
| } |
| function resetProgressStability(runId=null){ |
| progressStability.runId=runId; |
| progressStability.progress=0; |
| progressStability.timeline=null; |
| progressStability.furthestIndex=-1; |
| progressStability.status=''; |
| progressStability.currentStep=null; |
| progressMotionState.lastProgress=null; |
| progressMotionState.lastStatus=null; |
| progressMotionState.lastStep=null; |
| const timeline=$('timeline'); |
| 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';} |
| } |
| function stabilizeProgressPayload(p={},view=null){ |
| const runId=String(p.run_id||view?.run_id||state.runId||''); |
| if(progressStability.runId!==runId)resetProgressStability(runId); |
| const status=String(view?.header?.status||p.status||p.state?.status||'').toLowerCase(); |
| const terminal=progressIsTerminalStatus(status); |
| const success=progressIsSuccessStatus(status); |
| let rawProgress=Number(p.progress||0); |
| let rawTimeline=cloneTimeline(p.timeline||view?.pipeline||[]); |
| const rawIndex=timelineFurthestIndex(rawTimeline); |
|
|
| if(success){ |
| progressStability.progress=100; |
| progressStability.timeline=rawTimeline; |
| progressStability.furthestIndex=Math.max(progressStability.furthestIndex,rawIndex); |
| }else if(terminal){ |
| progressStability.progress=Math.max(progressStability.progress,rawProgress); |
| if(rawIndex>=progressStability.furthestIndex||!progressStability.timeline){ |
| progressStability.timeline=rawTimeline; |
| progressStability.furthestIndex=rawIndex; |
| }else{ |
| rawTimeline=cloneTimeline(progressStability.timeline); |
| } |
| }else{ |
| if(rawProgress>=progressStability.progress){ |
| progressStability.progress=rawProgress; |
| }else{ |
| rawProgress=progressStability.progress; |
| } |
| if(rawIndex>=progressStability.furthestIndex||!progressStability.timeline){ |
| progressStability.timeline=rawTimeline; |
| progressStability.furthestIndex=rawIndex; |
| }else{ |
| rawTimeline=cloneTimeline(progressStability.timeline); |
| } |
| } |
| const activeItem=activeTimelineItem(rawTimeline); |
| progressStability.currentStep=activeItem?.step||activeItem?.id||activeItem?.label||progressStability.currentStep; |
| progressStability.status=status; |
| return { |
| progress:success?100:Math.max(rawProgress,progressStability.progress), |
| timeline:rawTimeline, |
| status, |
| currentStep:activeItem?.step||activeItem?.id||activeItem?.label||'', |
| currentStepLabel:workerStepDisplayLabel(activeItem?.step||activeItem?.id)||activeItem?.label||'' |
| }; |
| } |
| function renderRunViewModel(view,p={}){ |
| const header=view.header||{}; |
| const sm=view.status_model||{}; |
| const stableEvents=mergeStableEvents(String(p.run_id||view.run_id||state.runId||''),p.events||[]); |
| const timelineModel=view.timeline_model||p.timeline_model||null; |
| const modelProgress=hasCanonicalTimelineModel(timelineModel)?(timelineModel.progress||{}):null; |
| const stable=stabilizeProgressPayload(p,view); |
| const canonical=canonicalRunUiState({view,...p}); |
| const progress=Number(modelProgress?.percent??stable.progress??(canonical.isTerminal?100:(header.status==='succeeded'?100:0))); |
| const runStatus=canonical.status; |
| const visualRunStatus=canonical.visual_status||runStatus; |
| const activePhase=hasCanonicalTimelineModel(timelineModel)?timelineModelActivePhase(timelineModel):null; |
| |
| animateProgressBar(progress,visualRunStatus,activePhase?.id||stable.currentStep); |
| setText('progressPercent',`${Math.round(progress)}%`); |
| renderSemanticProgressSummary(modelProgress,progress,visualRunStatus); |
| setText('progressTitle',canonical.label||header.title||view.run_id||'Live job progress'); |
| const statusEl=$('progressStatus'); |
| if(statusEl){ |
| statusEl.textContent=canonical.label||badgeLabel(runStatus||'Running'); |
| statusEl.className='badge '+statusClass(visualRunStatus); |
| animateStatusBadge(visualRunStatus); |
| } |
| const latestEvent=stableEvents[stableEvents.length-1]; |
| |
| setText('currentStep',activePhase?`${activePhase.label}: ${activePhase.summary||activePhase.status}`:(stable.currentStepLabel||p.current_step_label||'—')); |
| setText('lastEvent',modelProgress?.subtitle||eventMessage(latestEvent)||p.last_event||'Waiting for worker events.'); |
| setElapsedClock(p.elapsed_seconds??header.elapsed_seconds,canonical.statusLower||runStatus); |
| setText('lastPolled',new Date().toLocaleTimeString()); |
| setText('detailSpace',header.space||'—'); |
| setText('detailJobStatus',canonical.label||badgeLabel(runStatus||'—')); |
| setText('detailGateStatus',sm.verdict||'—'); |
| setText('detailTraces',Array.isArray(p.events)?`${p.events.length} events`:'—'); |
| const cancel=$('cancelRun'); |
| if(cancel){ |
| const s=String(runStatus||'').toLowerCase(); |
| const terminal=TERMINAL_STATUSES.has(s)||progressIsTerminalStatus(s); |
| cancel.hidden=Boolean(terminal); |
| cancel.disabled=!(state.runId&&view.links?.job_url&&!terminal); |
| } |
| if(!renderTimelineModel(timelineModel,runStatus))renderTimelinePlaceholder('Waiting for canonical run timeline…'); |
| renderActivityFeed(Array.isArray(view.activity)&&view.activity.length?view.activity:stableEvents); |
| renderDiagnostics(view.diagnostics||{},header); |
| renderSpaceTestPreview(view.space_test||{}); |
| renderManualAction({view,...p}); |
| if(typeof renderAgentRecovery==='function')renderAgentRecovery(p); |
| if(typeof renderRunDocuments==='function')renderRunDocuments(p); |
| } |
| function renderProgress(p){ |
| if(p.view){ |
| renderRunViewModel(p.view,p); |
| return; |
| } |
| const stableEvents=mergeStableEvents(String(p.run_id||state.runId||''),p.events||[]); |
| const stable=stabilizeProgressPayload(p,null); |
| const progress=Number(stable.progress||0); |
| animateProgressBar(progress,p.visual_status||p.status,stable.currentStep); |
| setText('progressPercent',`${Math.round(progress)}%`); |
| renderSemanticProgressSummary(null,progress,p.visual_status||p.status); |
| const latestEvent=stableEvents[stableEvents.length-1]; |
| setText('currentStep',stable.currentStepLabel||p.current_step_label||'—'); |
| setText('lastEvent',eventMessage(latestEvent)||p.last_event||'—'); |
| setElapsedClock(p.elapsed_seconds,p.status); |
| setText('progressTitle',p.current_step_label||'Live job progress'); |
| const statusEl=$('progressStatus'); |
| if(statusEl){ |
| statusEl.textContent=p.status||'running'; |
| statusEl.className='badge '+statusClass(p.visual_status||p.status); |
| animateStatusBadge(p.visual_status||p.status); |
| } |
| setText('lastPolled',new Date().toLocaleTimeString()); |
| renderTimelinePlaceholder('Waiting for canonical run timeline…'); |
| if(typeof renderAgentRecovery==='function')renderAgentRecovery(p); |
| } |
|
|
|
|
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|