Upload 6 files
Browse files- web/static/app.css +105 -1
- web/static/app.js +1 -1
- 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
function
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|