fffiloni commited on
Commit
3cbad46
Β·
verified Β·
1 Parent(s): 887efe8

Upload 6 files

Browse files
Files changed (3) hide show
  1. web/static/app.css +105 -1
  2. web/static/app.js +1 -1
  3. web/static/progress.js +183 -8
web/static/app.css CHANGED
@@ -193,7 +193,7 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
193
  @media (max-width: 720px){
194
  .app-shell{padding:12px}
195
  .topbar,.workspace-grid,.space-test-grid,.editor-grid,.test-top-row,.two-cols,.active-meta,.selected-grid,.validation-grid,.bucket-actions{grid-template-columns:1fr}
196
- .top-actions,.quick-links,.inline-actions{display:grid;grid-template-columns:1fr;align-items:stretch}
197
  .primary,.secondary,.link-btn,.bucket-pill,.user-chip,.logout-link,.filter{width:100%;white-space:normal}
198
  .timeline{grid-template-columns:1fr}
199
  .step{grid-template-columns:32px minmax(0,1fr);text-align:left;align-items:center;padding:8px;border:1px solid var(--line);border-radius:12px;background:#fff}
@@ -213,3 +213,107 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
213
  @media (min-width:721px){
214
  .timeline{grid-template-columns:repeat(26,minmax(72px,1fr))}
215
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  @media (max-width: 720px){
194
  .app-shell{padding:12px}
195
  .topbar,.workspace-grid,.space-test-grid,.editor-grid,.test-top-row,.two-cols,.active-meta,.selected-grid,.validation-grid,.bucket-actions{grid-template-columns:1fr}
196
+ .top-actions,.quick-links,.inline-actions,.active-run-actions{display:grid;grid-template-columns:1fr;align-items:stretch}
197
  .primary,.secondary,.link-btn,.bucket-pill,.user-chip,.logout-link,.filter{width:100%;white-space:normal}
198
  .timeline{grid-template-columns:1fr}
199
  .step{grid-template-columns:32px minmax(0,1fr);text-align:left;align-items:center;padding:8px;border:1px solid var(--line);border-radius:12px;background:#fff}
 
213
  @media (min-width:721px){
214
  .timeline{grid-template-columns:repeat(26,minmax(72px,1fr))}
215
  }
216
+
217
+ .active-run-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
218
+ .active-run-actions .link-btn{padding:8px 10px;font-size:12px}
219
+ .active-run-actions #cancelRun{margin-left:auto}
220
+
221
+
222
+ /* v56 feedback motion */
223
+ .progress-bar{position:relative}
224
+ .progress-bar>div{
225
+ transition:width .75s cubic-bezier(.22,1,.36,1), background-color .25s ease, filter .25s ease;
226
+ will-change:width,transform,filter;
227
+ position:relative;
228
+ overflow:hidden;
229
+ }
230
+ .progress-bar>div.is-running::after{
231
+ content:"";
232
+ position:absolute;
233
+ inset:0;
234
+ background:linear-gradient(90deg,transparent,rgba(255,255,255,.45),transparent);
235
+ transform:translateX(-100%);
236
+ animation:progress-shimmer 1.4s linear infinite;
237
+ }
238
+ .progress-bar>div.progress-bump{
239
+ animation:progress-bump .55s ease-out;
240
+ }
241
+ .value-bump{
242
+ animation:value-bump .55s ease-out;
243
+ }
244
+ .badge.status-bump{
245
+ animation:status-bump .7s ease-out;
246
+ }
247
+ .step{
248
+ transition:background-color .25s ease,border-color .25s ease,transform .25s ease,opacity .25s ease;
249
+ }
250
+ .step:before{
251
+ transition:background .55s ease;
252
+ }
253
+ .step-dot{
254
+ transition:background-color .35s ease,border-color .35s ease,color .35s ease,box-shadow .35s ease,transform .35s ease;
255
+ }
256
+ .step.running .step-dot{
257
+ animation:step-running-pulse 1.25s ease-in-out infinite;
258
+ }
259
+ .step.status-flash{
260
+ animation:step-status-flash .9s ease-out;
261
+ }
262
+ .step-dot.dot-pop{
263
+ animation:dot-pop .55s cubic-bezier(.34,1.56,.64,1);
264
+ }
265
+ .activity-row{
266
+ animation:activity-enter .28s ease-out;
267
+ }
268
+ @keyframes progress-shimmer{
269
+ to{transform:translateX(100%)}
270
+ }
271
+ @keyframes progress-bump{
272
+ 0%{filter:brightness(1);transform:scaleY(1)}
273
+ 40%{filter:brightness(1.18);transform:scaleY(1.35)}
274
+ 100%{filter:brightness(1);transform:scaleY(1)}
275
+ }
276
+ @keyframes value-bump{
277
+ 0%{transform:translateY(0);color:inherit}
278
+ 40%{transform:translateY(-2px);color:var(--blue)}
279
+ 100%{transform:translateY(0);color:inherit}
280
+ }
281
+ @keyframes status-bump{
282
+ 0%{transform:scale(1)}
283
+ 35%{transform:scale(1.06)}
284
+ 100%{transform:scale(1)}
285
+ }
286
+ @keyframes step-running-pulse{
287
+ 0%,100%{box-shadow:0 0 0 4px #eff6ff}
288
+ 50%{box-shadow:0 0 0 8px rgba(37,99,235,.16)}
289
+ }
290
+ @keyframes step-status-flash{
291
+ 0%{background:rgba(37,99,235,.12);transform:translateY(-1px)}
292
+ 100%{background:transparent;transform:translateY(0)}
293
+ }
294
+ @keyframes dot-pop{
295
+ 0%{transform:scale(.72)}
296
+ 55%{transform:scale(1.2)}
297
+ 100%{transform:scale(1)}
298
+ }
299
+ @keyframes activity-enter{
300
+ from{opacity:.2;transform:translateY(-4px)}
301
+ to{opacity:1;transform:translateY(0)}
302
+ }
303
+ @media (prefers-reduced-motion: reduce){
304
+ .progress-bar>div,
305
+ .progress-bar>div::after,
306
+ .step,
307
+ .step:before,
308
+ .step-dot,
309
+ .activity-row,
310
+ .badge,
311
+ .value-bump{
312
+ transition:none!important;
313
+ animation:none!important;
314
+ }
315
+ }
316
+
317
+ .pi-trace-panel summary{cursor:pointer;font-weight:800;font-size:13px}
318
+ .pi-trace-output{background:#0b1220;color:#dbeafe;min-height:180px}
319
+ .pi-trace-output::selection{background:#1d4ed8;color:#fff}
web/static/app.js CHANGED
@@ -18,7 +18,7 @@ function validateBuildForm(){if(!state.bucketReady)return'Create or check your p
18
  function validateValidationForm(){if(!formValue('validateSpace'))return'Enter the target Space ID to validate.';try{JSON.parse(formValue('testArgs')||'[]');JSON.parse(formValue('testKwargs')||'{}')}catch(e){return`Invalid JSON payload: ${e.message}`}return''}
19
  function setBuildModeNew(){const panel=$('buildModePanel');if(panel){panel.classList.remove('inspecting');panel.innerHTML='<strong>Preparing a new build</strong><p>This form launches new builds. Existing runs can be inspected in the Runs Explorer on the right.</p>'}const launch=$('launchResultPanel');if(launch)launch.hidden=true;const status=$('progressStatus');if(status){status.textContent='Idle';status.className='badge neutral'}setText('progressTitle','No run selected')}
20
  function setBuildModeInspecting(runId){const panel=$('buildModePanel');if(panel){panel.classList.add('inspecting');panel.innerHTML=`<strong>Inspecting selected run</strong><p>You are viewing run <code>${escapeHtml(runId||'')}</code>. The build form is unchanged and the active run panel is now tracking it.</p>`}}
21
- function resetRunDetails(){for(const [id,value] of [['detailRunId','β€”'],['detailJobStatus','β€”'],['detailGateStatus','β€”'],['detailSpace','β€”'],['detailHardware','β€”'],['detailTraces','β€”'],['homeRunDetailStatus','No run'],['selectedRunId','β€”'],['selectedModelId','β€”'],['selectedTargetSpace','β€”'],['selectedHardware','β€”'],['runDetailStatus','β€”']])setText(id,value);setQuickLinks({});const report=$('homeReportPreview');if(report)report.innerHTML='<strong>No run selected</strong><p>Select a run to preview its report and actions.</p>';const full=$('reportPreview');if(full)full.textContent='No report selected.';const manual=$('manualActionPanel');if(manual)manual.hidden=true;const activity=$('activityFeed');if(activity)activity.innerHTML='<div class="empty">No activity yet.</div>'}
22
  function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.selectedRunDetail=null;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','β€”');setText('lastPolled','β€”');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
23
  function renderLaunchResult(result={}){const panel=$('launchResultPanel');if(panel)panel.hidden=false;setText('launchRunId',result.run_id||'β€”');setText('launchStatus',result.status||'running');setText('launchSpace',result.target_space||'β€”');setText('launchHardware',result.selected_hardware||result.preferred_space_hardware||'β€”');const links=normalizeLinks({...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});setLink('launchOpenJob',links.job_url);setLink('launchOpenSpace',links.target_space_url);setLink('launchOpenSettings',links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');setLink('launchOpenArtifacts',links.artifacts_url)}function setActiveRun(result){state.runId=result.run_id;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||'β€”');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});renderLaunchResult(result);saveActiveRun();startPolling(result.run_id)}
24
  function startPolling(runId){state.runId=runId;state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;if(state.poll)clearInterval(state.poll);saveActiveRun();refreshProgress();state.poll=setInterval(refreshProgress,2500)}
 
18
  function validateValidationForm(){if(!formValue('validateSpace'))return'Enter the target Space ID to validate.';try{JSON.parse(formValue('testArgs')||'[]');JSON.parse(formValue('testKwargs')||'{}')}catch(e){return`Invalid JSON payload: ${e.message}`}return''}
19
  function setBuildModeNew(){const panel=$('buildModePanel');if(panel){panel.classList.remove('inspecting');panel.innerHTML='<strong>Preparing a new build</strong><p>This form launches new builds. Existing runs can be inspected in the Runs Explorer on the right.</p>'}const launch=$('launchResultPanel');if(launch)launch.hidden=true;const status=$('progressStatus');if(status){status.textContent='Idle';status.className='badge neutral'}setText('progressTitle','No run selected')}
20
  function setBuildModeInspecting(runId){const panel=$('buildModePanel');if(panel){panel.classList.add('inspecting');panel.innerHTML=`<strong>Inspecting selected run</strong><p>You are viewing run <code>${escapeHtml(runId||'')}</code>. The build form is unchanged and the active run panel is now tracking it.</p>`}}
21
+ function resetRunDetails(){const cancel=$('cancelRun');if(cancel)cancel.disabled=true;for(const [id,value] of [['detailRunId','β€”'],['detailJobStatus','β€”'],['detailGateStatus','β€”'],['detailSpace','β€”'],['detailHardware','β€”'],['detailTraces','β€”'],['homeRunDetailStatus','No run'],['selectedRunId','β€”'],['selectedModelId','β€”'],['selectedTargetSpace','β€”'],['selectedHardware','β€”'],['runDetailStatus','β€”']])setText(id,value);setQuickLinks({});const report=$('homeReportPreview');if(report)report.innerHTML='<strong>No run selected</strong><p>Select a run to preview its report and actions.</p>';const full=$('reportPreview');if(full)full.textContent='No report selected.';const manual=$('manualActionPanel');if(manual)manual.hidden=true;const activity=$('activityFeed');if(activity)activity.innerHTML='<div class="empty">No activity yet.</div>'}
22
  function prepareNewBuild(){if(state.poll){clearInterval(state.poll);state.poll=null}state.runId=null;state.selectedRunDetail=null;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','β€”');setText('lastPolled','β€”');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
23
  function renderLaunchResult(result={}){const panel=$('launchResultPanel');if(panel)panel.hidden=false;setText('launchRunId',result.run_id||'β€”');setText('launchStatus',result.status||'running');setText('launchSpace',result.target_space||'β€”');setText('launchHardware',result.selected_hardware||result.preferred_space_hardware||'β€”');const links=normalizeLinks({...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});setLink('launchOpenJob',links.job_url);setLink('launchOpenSpace',links.target_space_url);setLink('launchOpenSettings',links.target_space_settings_url||links.target_space_url&&links.target_space_url+'/settings');setLink('launchOpenArtifacts',links.artifacts_url)}function setActiveRun(result){state.runId=result.run_id;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||'β€”');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});renderLaunchResult(result);saveActiveRun();startPolling(result.run_id)}
24
  function startPolling(runId){state.runId=runId;state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;if(state.poll)clearInterval(state.poll);saveActiveRun();refreshProgress();state.poll=setInterval(refreshProgress,2500)}
web/static/progress.js CHANGED
@@ -1,8 +1,183 @@
1
- function timelineIcon(status){status=String(status||'pending');if(status==='done'||status==='completed')return'βœ“';if(status==='running')return'β€’';if(status==='failed'||status==='blocked')return'!';if(status==='skipped')return'β†·';return'β—‹'}
2
- function timelineStatus(status){status=String(status||'pending');if(status==='done'||status==='completed')return'completed';if(status==='running')return'running';if(status==='failed')return'failed';if(status==='blocked')return'blocked';return'pending'}
3
- function renderTimeline(items){const root=$('timeline');if(!root)return;root.innerHTML=(items||[]).map((x,i)=>{const st=timelineStatus(x.status);return `<div class="step ${st}" data-status="${st}"><div class="step-dot">${timelineIcon(x.status)}</div><div><strong>${escapeHtml(x.label||`Step ${i+1}`)}</strong><small>${st}</small></div></div>`}).join('')}
4
- function renderActivityFeed(items){const root=$('activityFeed');if(!root)return;if(!items.length){root.innerHTML='<div class="empty">No activity yet. Waiting for the first worker event.</div>';return}root.innerHTML=items.slice(-8).reverse().map(item=>`<div class="activity-row ${statusClass(item.severity||item.status)}"><time>${escapeHtml(formatTime(item.ts))}</time><span class="activity-dot"></span><div><strong>${escapeHtml(item.message||'Event received')}</strong><p>${escapeHtml(item.step||'event')} Β· ${escapeHtml(item.status||'')}</p></div><em>${escapeHtml(item.agent||'Factory Agent')}</em></div>`).join('')}
5
- 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('')}
6
- function renderSpaceTestPreview(s={}){const st=$('spaceTestStatus');if(st){st.textContent=badgeLabel(s.status||'Pending');st.className='badge '+statusClass(s.status)}setText('spaceTestVerdict',badgeLabel(s.verdict||'pending'));setText('spaceTestOutput',s.output_artifact?'Output artifact ready':'Waiting for API test')}
7
- function renderRunViewModel(view,p={}){const header=view.header||{};const sm=view.status_model||{};const progress=Number(sm.global_status==='succeeded'?100:(p.progress||0));const fill=$('progressFill');if(fill)fill.style.width=`${Math.max(0,Math.min(100,progress))}%`;setText('progressPercent',`${Math.round(progress)}%`);setText('progressTitle',header.title||view.run_id||'Live job progress');const statusEl=$('progressStatus');if(statusEl){statusEl.textContent=header.status_label||badgeLabel(header.status||'Running');statusEl.className='badge '+statusClass(header.status)}setText('currentStep',header.current_phase_label||'β€”');setText('lastEvent',(view.activity||[]).slice(-1)[0]?.message||p.last_event||'Waiting for worker events.');setText('elapsed',fmtSeconds(header.elapsed_seconds||p.elapsed_seconds));setText('lastPolled',new Date().toLocaleTimeString());setText('detailSpace',header.space||'β€”');setText('detailJobStatus',header.raw_status||header.status||'β€”');setText('detailGateStatus',sm.verdict||'β€”');setText('detailTraces',Array.isArray(p.events)?`${p.events.length} events`:'β€”');setLink('openJob',view.links?.job_url);setLink('openSpace',view.links?.target_space_url);setLink('openSettings',view.links?.target_space_settings_url);setLink('openArtifacts',view.links?.artifacts_url);renderTimeline(p.timeline||view.pipeline||[]);renderActivityFeed(view.activity||[]);renderDiagnostics(view.diagnostics||{},header);renderSpaceTestPreview(view.space_test||{});renderManualAction({view,...p})}
8
- function renderProgress(p){if(p.view){renderRunViewModel(p.view,p);return}const progress=Number(p.progress||0);const fill=$('progressFill');if(fill)fill.style.width=`${Math.max(0,Math.min(100,progress))}%`;setText('progressPercent',`${progress}%`);setText('currentStep',p.current_step_label||'β€”');setText('lastEvent',p.last_event||'β€”');setText('elapsed',fmtSeconds(p.elapsed_seconds));setText('progressTitle',p.current_step_label||'Live job progress');const statusEl=$('progressStatus');if(statusEl){statusEl.textContent=p.status||'running';statusEl.className='badge '+statusClass(p.status)}setText('lastPolled',new Date().toLocaleTimeString());renderTimeline(p.timeline||[])}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const timelineRenderState={};
2
+ const progressMotionState={lastProgress:null,lastStatus:null,lastStep:null};
3
+
4
+ function timelineIcon(status){
5
+ status=String(status||'pending');
6
+ if(status==='done'||status==='completed')return'βœ“';
7
+ if(status==='running')return'β€’';
8
+ if(status==='failed'||status==='blocked')return'!';
9
+ if(status==='skipped')return'β†·';
10
+ return'β—‹';
11
+ }
12
+ function timelineStatus(status){
13
+ status=String(status||'pending');
14
+ if(status==='done'||status==='completed')return'completed';
15
+ if(status==='running')return'running';
16
+ if(status==='failed')return'failed';
17
+ if(status==='blocked')return'blocked';
18
+ return'pending';
19
+ }
20
+ function timelineStepKey(x,i){
21
+ return String(x.step||x.id||x.label||`step-${i}`);
22
+ }
23
+ function animateClass(el,klass,duration=900){
24
+ if(!el)return;
25
+ el.classList.remove(klass);
26
+ void el.offsetWidth;
27
+ el.classList.add(klass);
28
+ window.setTimeout(()=>el.classList.remove(klass),duration);
29
+ }
30
+ function renderTimeline(items){
31
+ const root=$('timeline');
32
+ if(!root)return;
33
+ const list=items||[];
34
+ const nextState={};
35
+ root.innerHTML=list.map((x,i)=>{
36
+ const key=timelineStepKey(x,i);
37
+ const st=timelineStatus(x.status);
38
+ const prev=timelineRenderState[key];
39
+ const changed=prev&&prev!==st;
40
+ nextState[key]=st;
41
+ return `<div class="step ${st}${changed?' status-changed':''}" data-step="${escapeHtml(key)}" data-status="${st}"><div class="step-dot">${timelineIcon(x.status)}</div><div><strong>${escapeHtml(x.label||`Step ${i+1}`)}</strong><small>${st}</small></div></div>`;
42
+ }).join('');
43
+ Object.keys(timelineRenderState).forEach(k=>delete timelineRenderState[k]);
44
+ Object.assign(timelineRenderState,nextState);
45
+ root.querySelectorAll('.step.status-changed').forEach(step=>{
46
+ animateClass(step,'status-flash',1000);
47
+ const dot=step.querySelector('.step-dot');
48
+ animateClass(dot,'dot-pop',700);
49
+ });
50
+ }
51
+ function renderActivityFeed(items){
52
+ const root=$('activityFeed');
53
+ if(!root)return;
54
+ if(!items.length){
55
+ root.innerHTML='<div class="empty">No activity yet. Waiting for the first worker event.</div>';
56
+ return;
57
+ }
58
+ root.innerHTML=items.slice(-8).reverse().map(item=>`<div class="activity-row ${statusClass(item.severity||item.status)}"><time>${escapeHtml(formatTime(item.ts))}</time><span class="activity-dot"></span><div><strong>${escapeHtml(item.message||'Event received')}</strong><p>${escapeHtml(item.step||'event')} Β· ${escapeHtml(item.status||'')}</p></div><em>${escapeHtml(item.agent||'Factory Agent')}</em></div>`).join('');
59
+ }
60
+ function renderDiagnostics(d={},header={}){
61
+ setText('diagBuildStatus',badgeLabel(d.build_status||'pending'));
62
+ setText('diagBuildStep',header.current_phase_label||'Waiting for run data');
63
+ const chips=$('statusChips');
64
+ if(chips){
65
+ const data=[['Build',d.build_status],['API',d.api_status],['Tests',d.tests_status],['Verdict',d.verdict_status]];
66
+ chips.innerHTML=data.map(([k,v])=>`<span class="status-chip ${statusClass(v)}"><strong>${escapeHtml(k)}</strong><em>${escapeHtml(badgeLabel(v||'pending'))}</em></span>`).join('');
67
+ }
68
+ const issue=$('issueCard');
69
+ if(issue){
70
+ const has=Boolean(d.issue&&d.issue.title);
71
+ issue.hidden=!has;
72
+ if(has){
73
+ issue.className='issue-card '+statusClass(d.issue.status||d.issue.title);
74
+ setText('issueTitle',d.issue.title);
75
+ setText('issueDetail',d.issue.detail||d.issue.status||'Review logs.');
76
+ }
77
+ }
78
+ const rules=$('zeroGpuChecklist');
79
+ 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('');
80
+ }
81
+ function renderSpaceTestPreview(s={}){
82
+ const st=$('spaceTestStatus');
83
+ if(st){
84
+ st.textContent=badgeLabel(s.status||'pending');
85
+ st.className='badge '+statusClass(s.status);
86
+ }
87
+ setText('spaceTestOutput',s.output_artifact||s.endpoint||'Waiting for API test');
88
+ setText('spaceTestVerdict',s.verdict||'pending');
89
+ }
90
+ function animateProgressBar(progress,status,currentStep){
91
+ const fill=$('progressFill');
92
+ if(!fill)return;
93
+ const value=Math.max(0,Math.min(100,Number(progress||0)));
94
+ const previous=progressMotionState.lastProgress;
95
+ fill.style.setProperty('--progress-target',`${value}%`);
96
+ fill.style.width=`${value}%`;
97
+ fill.classList.toggle('is-running',String(status||'').toLowerCase()==='running');
98
+ if(previous!==null&&Math.round(previous)!==Math.round(value)){
99
+ animateClass(fill,'progress-bump',650);
100
+ const pct=$('progressPercent');
101
+ animateClass(pct,'value-bump',650);
102
+ }
103
+ progressMotionState.lastProgress=value;
104
+ progressMotionState.lastStep=currentStep||progressMotionState.lastStep;
105
+ }
106
+ function animateStatusBadge(status){
107
+ const el=$('progressStatus');
108
+ if(!el)return;
109
+ const next=String(status||'').toLowerCase();
110
+ if(progressMotionState.lastStatus&&progressMotionState.lastStatus!==next){
111
+ animateClass(el,'status-bump',800);
112
+ }
113
+ progressMotionState.lastStatus=next;
114
+ }
115
+ function renderPiLiveTrace(text){
116
+ const root=$('piTracePanel');
117
+ if(!root)return;
118
+ const value=String(text||'').trim();
119
+ if(!value){
120
+ root.textContent='No Pi trace yet. It will appear while Qwen Coder is working through Pi.';
121
+ return;
122
+ }
123
+ root.textContent=value;
124
+ root.scrollTop=root.scrollHeight;
125
+ }
126
+ function renderRunViewModel(view,p={}){
127
+ const header=view.header||{};
128
+ const sm=view.status_model||{};
129
+ const progress=Number(p.progress??(header.status==='succeeded'?100:0));
130
+ animateProgressBar(progress,header.status||p.status,header.current_phase);
131
+ setText('progressPercent',`${Math.round(progress)}%`);
132
+ setText('progressTitle',header.title||view.run_id||'Live job progress');
133
+ const statusEl=$('progressStatus');
134
+ if(statusEl){
135
+ statusEl.textContent=header.status_label||badgeLabel(header.status||'Running');
136
+ statusEl.className='badge '+statusClass(header.status);
137
+ animateStatusBadge(header.status);
138
+ }
139
+ setText('currentStep',header.current_phase_label||'β€”');
140
+ setText('lastEvent',(view.activity||[]).slice(-1)[0]?.message||p.last_event||'Waiting for worker events.');
141
+ setText('elapsed',fmtSeconds(p.elapsed_seconds??header.elapsed_seconds));
142
+ setText('lastPolled',new Date().toLocaleTimeString());
143
+ setText('detailSpace',header.space||'β€”');
144
+ setText('detailJobStatus',header.raw_status||header.status||'β€”');
145
+ setText('detailGateStatus',sm.verdict||'β€”');
146
+ setText('detailTraces',Array.isArray(p.events)?`${p.events.length} events`:'β€”');
147
+ setLink('openJob',view.links?.job_url);
148
+ setLink('openSpace',view.links?.target_space_url);
149
+ setLink('openSettings',view.links?.target_space_settings_url);
150
+ setLink('openArtifacts',view.links?.artifacts_url);
151
+ const cancel=$('cancelRun');
152
+ if(cancel){
153
+ const s=String(header.status||p.status||'').toLowerCase();
154
+ cancel.disabled=!(state.runId&&view.links?.job_url&&!TERMINAL_STATUSES.has(s));
155
+ }
156
+ renderTimeline(p.timeline||view.pipeline||[]);
157
+ renderActivityFeed(view.activity||[]);
158
+ renderDiagnostics(view.diagnostics||{},header);
159
+ renderSpaceTestPreview(view.space_test||{});
160
+ renderManualAction({view,...p});
161
+ renderPiLiveTrace(p.pi_live_log||'');
162
+ }
163
+ function renderProgress(p){
164
+ if(p.view){
165
+ renderRunViewModel(p.view,p);
166
+ return;
167
+ }
168
+ const progress=Number(p.progress||0);
169
+ animateProgressBar(progress,p.status,p.current_step);
170
+ setText('progressPercent',`${Math.round(progress)}%`);
171
+ setText('currentStep',p.current_step_label||'β€”');
172
+ setText('lastEvent',p.last_event||'β€”');
173
+ setText('elapsed',fmtSeconds(p.elapsed_seconds));
174
+ setText('progressTitle',p.current_step_label||'Live job progress');
175
+ const statusEl=$('progressStatus');
176
+ if(statusEl){
177
+ statusEl.textContent=p.status||'running';
178
+ statusEl.className='badge '+statusClass(p.status);
179
+ animateStatusBadge(p.status);
180
+ }
181
+ setText('lastPolled',new Date().toLocaleTimeString());
182
+ renderTimeline(p.timeline||[]);
183
+ }