fffiloni commited on
Commit
c778235
·
verified ·
1 Parent(s): 6c62410

Upload 6 files

Browse files
Files changed (3) hide show
  1. web/static/app.css +4 -0
  2. web/static/app.js +6 -6
  3. web/static/progress.js +26 -25
web/static/app.css CHANGED
@@ -379,3 +379,7 @@ strong,p,h1,h2,h3,h4,span,a,button,div,li{overflow-wrap:break-word;word-break:no
379
  max-height:235px;
380
  overflow-y:auto;
381
  }
 
 
 
 
 
379
  max-height:235px;
380
  overflow-y:auto;
381
  }
382
+
383
+ /* v70: simpler events list and visible-only timeline scrolling */
384
+ .activity-feed.show-all{max-height:184px;overflow-y:auto;scrollbar-width:thin}
385
+ .timeline{scroll-behavior:smooth}
web/static/app.js CHANGED
@@ -21,7 +21,7 @@ function setBuildModeInspecting(runId){const panel=$('buildModePanel');if(panel)
21
  function resetRunDetails(){
22
  const cancel=$('cancelRun');if(cancel)cancel.disabled=true;
23
  if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;if(typeof resetProgressStability==='function')resetProgressStability(null);
24
- if(typeof activityFeedState!=='undefined'){activityFeedState.showAll=false;activityFeedState.lastItems=[];activityFeedState.runId=null}
25
  for(const [id,value] of [
26
  ['detailRunId','—'],['detailJobStatus','—'],['detailGateStatus','—'],['detailSpace','—'],['detailHardware','—'],['detailTraces','—'],
27
  ['homeRunDetailStatus','No run'],['selectedRunId','—'],['selectedModelId','—'],['selectedModelShort','—'],['selectedTargetSpace','—'],
@@ -37,7 +37,7 @@ function resetRunDetails(){
37
  const manual=$('manualActionPanel');if(manual)manual.hidden=true;
38
  const warning=$('pollWarning');if(warning)warning.hidden=true;
39
  const activity=$('activityFeed');if(activity){activity.dataset.signature='';activity.classList.remove('show-all');activity.innerHTML='<div class="empty">No activity yet.</div>'}
40
- const viewAll=$('viewAllEvents');if(viewAll)viewAll.textContent='View all events';
41
  const events=$('eventsPanel');if(events)events.textContent='No events yet.';
42
  const artifacts=$('artifactList');if(artifacts)artifacts.textContent='No artifacts indexed yet.';
43
  const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''}
@@ -45,7 +45,7 @@ function resetRunDetails(){
45
  renderTimeline(DEFAULT_TIMELINE,'idle');
46
  } 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','—');if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;setText('lastPolled','—');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
47
  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)}
48
- function startPolling(runId){state.runId=runId;state.pollErrors=0;state.lastJobLogPoll=0;if(typeof resetProgressStability==='function')resetProgressStability(runId);const warn=$('pollWarning');if(warn)warn.hidden=true;if(state.poll)clearInterval(state.poll);saveActiveRun();refreshProgress();state.poll=setInterval(refreshProgress,2500)}
49
  async function bootstrapRunRecovery(){return restoreActiveRun()}
50
  async function restoreActiveRun(){try{const resumable=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(resumable?.run?.run_id){state.runId=resumable.run.run_id;if(resumable.bucket_source)state.bucketSource=resumable.bucket_source;setBuildModeInspecting(state.runId);if(resumable.view)renderRunViewModel(resumable.view,{progress:0,events:[]});setQuickLinks(resumable.view?.links||{});startPolling(state.runId);return true}}catch(e){console.warn('resumable lookup failed',e)}try{const raw=localStorage.getItem('asf.activeRun');if(!raw)return false;const saved=JSON.parse(raw);if(saved?.bucketName)state.bucketName=saved.bucketName;if(saved?.runId){state.runId=saved.runId;setText('detailRunId',saved.runId);setQuickLinks(saved.links||{});setBuildModeInspecting(saved.runId);startPolling(saved.runId);return true}}catch(_){}return false}
51
  async function loadAppInfo(){setOperation('Checking sign-in and run storage…','loading');try{const me=await apiGet('/api/me');state.user=me;setText('userChip','@'+me.username);const userChip=$('userChip');if(userChip)userChip.href=`https://huggingface.co/${me.username}`;const logout=$('logoutLink');if(logout){logout.hidden=false;logout.href=me.logout_url||'/logout'}state.bucketSource=me.username+'/'+state.bucketName;setText('bucketInlineLabel',state.bucketSource);renderAuthWarnings(me);await checkBucket(false);await loadRuns(true);clearSavedRun();if(!state.bucketReady)setOperation('Create your private run bucket before launching builds.','warning');else setOperation('')}catch(e){state.user=null;setText('userChip','Sign in');const userChip=$('userChip');if(userChip)userChip.href='/oauth/huggingface/login';const logout=$('logoutLink');if(logout)logout.hidden=true;renderSignedOut();setBucketReady(false,'Sign in with Hugging Face first.');showMessage('Please sign in with Hugging Face, then refresh this page.',true)}}
@@ -57,7 +57,7 @@ function renderManualAction(p={}){const panel=$('manualActionPanel');if(!panel)r
57
  function prepareValidationFromActiveRun(){const detail=state.selectedRunDetail;const summary=detail?.summary||{};const target=summary.target_space||$('detailSpace')?.textContent||'';if(target&&target!=='—')setInputValue('validateSpace',target);const expected=summary.expected_output_type||detail?.generation_smoke?.expected_output_type||formValue('buildExpectedOutput')||'image';setInputValue('expectedOutput',expected);setInputValue('validateApi','/generate');switchView('validate');showMessage(`Prepared validation form for ${target||'selected Space'}.`,'info')}
58
  async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,run_id:formValue('runId')||undefined,model_id:formValue('modelId'),target_space_name:formValue('targetSpace'),pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,expected_output_type:formValue('buildExpectedOutput')};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}}
59
  async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const payload={bucket_name:state.bucketName,target_space_id:formValue('validateSpace'),api_name:formValue('validateApi')||'/generate',expected_output_type:formValue('expectedOutput')||'image',test_args_json:formValue('testArgs')||'[]',test_kwargs_json:formValue('testKwargs')||'{}',live_timeout_seconds:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);setActiveRun(result);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
60
- async function refreshProgress(){if(!state.runId)return;try{const now=Date.now();const includeLogs=!state.lastJobLogPoll||now-state.lastJobLogPoll>12000;if(includeLogs)state.lastJobLogPoll=now;const p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}${includeLogs?'&include_job_logs=1':''}`);state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;if(p.bucket_source)state.bucketSource=p.bucket_source;renderProgress(p);renderEvents(p.events||[]);renderMetrics(p);if(p.view){setText('detailRunId',p.view.run_id||p.run_id);setText('selectedModelId',p.view.header?.model||p.summary?.model_id||'—')}else{setText('detailRunId',p.run_id||state.runId)}if(p.state){setText('detailSpace',p.state.target_space||p.summary?.target_space||'—');setText('detailHardware',p.state.selected_hardware||p.summary?.selected_hardware||'—')}setText('detailJobStatus',p.view?.header?.raw_status||p.state?.status||p.status||'running');setText('detailGateStatus',p.inference_gate?.status||p.summary?.gate_status||'—');setText('detailTraces',Array.isArray(p.events)?String(p.events.length)+' events':'—');setText('homeRunDetailStatus',p.view?.header?.status_label||p.state?.status||p.status||'running');setQuickLinks({...p.links,...p.view?.links,bucket_source:p.bucket_source,run_id:p.run_id});saveActiveRun();const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();if(TERMINAL_STATUSES.has(status)&&state.poll){clearInterval(state.poll);state.poll=null;setOperation(`Run finished with status: ${badgeLabel(status)}`,status.includes('success')||status.includes('succeed')?'success':status.includes('failed')||status.includes('blocker')?'error':'warning')}if(now-(state.lastRunsRefreshFromPoll||0)>15000){state.lastRunsRefreshFromPoll=now;loadRuns(false)}}catch(e){state.pollErrors+=1;console.warn(e);const warn=$('pollWarning');if(warn){warn.textContent=`Progress polling failed ${state.pollErrors} time${state.pollErrors===1?'':'s'}. The Job may still be running; retrying automatically…`;warn.hidden=false}}} function renderMetrics(p){const root=$('metricsList');if(!root)return;const smoke=p.generation_smoke||{};const gate=p.inference_gate||{};const health=(gate.implementation_signals&&gate.implementation_signals.health_passed)||smoke.health_passed;const smokeOk=smoke.ok||smoke.status==='success';const latency=smoke.latency_seconds||p.summary?.latency_seconds;const duration=smoke.recommended_zerogpu_duration_seconds||smoke.recommended_duration_seconds;root.innerHTML=`<li><span>/health</span><strong class="${health?'metric-ok':''}">${health?'passed':'pending'}</strong></li><li><span>/generate smoke test</span><strong class="${smokeOk?'metric-ok':'metric-warn'}">${smokeOk?'passed':(smoke.status||'pending')}</strong></li><li><span>Latency</span><strong>${latency?fmtSeconds(Math.round(latency)):'—'}</strong></li><li><span>Recommended ZeroGPU duration</span><strong>${duration?fmtSeconds(Math.round(duration)):'—'}</strong></li>`}
61
  function renderEvents(events){const root=$('eventsPanel');if(!root)return;root.textContent=(events||[]).slice(-12).map(e=>`${e.ts||''} ${e.step||''} ${e.status||''} ${e.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}
62
  async function cancelActiveRun(){
63
  if(!state.runId){showMessage('Select a running run before cancelling.', 'warning');return}
@@ -75,11 +75,11 @@ async function cancelActiveRun(){
75
  setButtonBusy('cancelRun',false);
76
  }
77
  }
78
- async function resumeLatestRun(){try{const data=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(!data.run){showMessage('No resumable run found in this bucket.','warning');return}state.runId=data.run.run_id;if(data.bucket_source)state.bucketSource=data.bucket_source;if(data.view)renderRunViewModel(data.view,{progress:0,events:[]});setBuildModeInspecting(state.runId);setQuickLinks(data.view?.links||{});startPolling(state.runId);showMessage(`Resumed ${state.runId}`,'success')}catch(e){showMessage(e.message,true)}}
79
  function switchView(view){state.currentView=view;document.querySelectorAll('.nav-item').forEach(b=>b.classList.toggle('active',b.dataset.view===view));document.querySelectorAll('[data-section]').forEach(s=>s.hidden=s.dataset.section!==view);setOperation('');const warn=$('pollWarning');if(warn)warn.hidden=true;if(view==='progress')loadRuns(false);const targetMap={build:'newBuildCard',progress:'runsExplorerPanel',validate:'spaceTestSection'};const target=$(targetMap[view]||'');if(target)target.scrollIntoView({behavior:'smooth',block:'start'})}
80
  function bindNavigation(){document.querySelectorAll('.nav-item').forEach(btn=>btn.addEventListener('click',()=>switchView(btn.dataset.view||'build')));document.querySelectorAll('[data-view-target]').forEach(btn=>btn.addEventListener('click',()=>switchView(btn.dataset.viewTarget||'build')))}
81
  function bindRunFilters(){document.querySelectorAll('.filter[data-status]').forEach(btn=>btn.addEventListener('click',()=>{state.runStatusFilter=btn.dataset.status||'all';state.runPage=0;renderRunsFromCache()}));for(const id of ['runSearch','runSearchFull'])$(id)?.addEventListener('input',e=>{state.runSearch=e.target.value||'';state.runPage=0;for(const other of ['runSearch','runSearchFull'])if($(other)&&$(other)!==e.target)$(other).value=state.runSearch;clearTimeout(state.runSearchTimer);state.runSearchTimer=setTimeout(renderRunsFromCache,80)})}
82
- function bindActions(){$('launchBuild')?.setAttribute('data-requires-bucket','true');$('launchBuild')?.setAttribute('data-action-button','true');$('launchValidate')?.setAttribute('data-action-button','true');$('launchBuild')?.addEventListener('click',launchBuild);$('launchValidate')?.addEventListener('click',launchValidate);$('prepareNewBuild')?.addEventListener('click',prepareNewBuild);$('manualGoValidate')?.addEventListener('click',prepareValidationFromActiveRun);$('homeValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('selectedValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('checkBucket')?.addEventListener('click',()=>checkBucket(true));$('bucketStatusPill')?.addEventListener('click',()=>checkBucket(true));$('createBucket')?.addEventListener('click',createBucket);$('refreshRuns')?.addEventListener('click',()=>loadRuns(true));$('refreshRunsHome')?.addEventListener('click',()=>loadRuns(true));$('resumeLatestRun')?.addEventListener('click',resumeLatestRun);$('cancelRun')?.addEventListener('click',cancelActiveRun);$('resumeLatestRunFull')?.addEventListener('click',resumeLatestRun);$('viewAllEvents')?.addEventListener('click',()=>{if(typeof toggleAllEvents==='function')toggleAllEvents()});['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>$(id)?.addEventListener('change',()=>setText('hardwarePlan',hardwarePlanLabel())))}
83
  bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();
84
 
85
  // Pass 47 intentionally removed automatic switchView('progress') after launch and run selection.
 
21
  function resetRunDetails(){
22
  const cancel=$('cancelRun');if(cancel)cancel.disabled=true;
23
  if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;if(typeof resetProgressStability==='function')resetProgressStability(null);
24
+ if(typeof activityFeedState!=='undefined'){activityFeedState.lastItems=[];activityFeedState.runId=null}
25
  for(const [id,value] of [
26
  ['detailRunId','—'],['detailJobStatus','—'],['detailGateStatus','—'],['detailSpace','—'],['detailHardware','—'],['detailTraces','—'],
27
  ['homeRunDetailStatus','No run'],['selectedRunId','—'],['selectedModelId','—'],['selectedModelShort','—'],['selectedTargetSpace','—'],
 
37
  const manual=$('manualActionPanel');if(manual)manual.hidden=true;
38
  const warning=$('pollWarning');if(warning)warning.hidden=true;
39
  const activity=$('activityFeed');if(activity){activity.dataset.signature='';activity.classList.remove('show-all');activity.innerHTML='<div class="empty">No activity yet.</div>'}
40
+
41
  const events=$('eventsPanel');if(events)events.textContent='No events yet.';
42
  const artifacts=$('artifactList');if(artifacts)artifacts.textContent='No artifacts indexed yet.';
43
  const blockers=$('selectedBlockers');if(blockers){blockers.hidden=true;blockers.innerHTML=''}
 
45
  renderTimeline(DEFAULT_TIMELINE,'idle');
46
  } 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','—');if(typeof elapsedClock!=='undefined')elapsedClock.isRunning=false;setText('lastPolled','—');setText('hardwarePlan',hardwarePlanLabel());renderMetrics({});showMessage('New build mode ready.','info')}
47
  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)}
48
+ function startPolling(runId){state.runId=runId;state.pollErrors=0;state.lastJobLogPoll=0;if(typeof resetProgressStability==='function')resetProgressStability(runId);const warn=$('pollWarning');if(warn)warn.hidden=true;if(state.poll)clearInterval(state.poll);saveActiveRun();refreshProgress();state.poll=setInterval(refreshProgress,2000)}
49
  async function bootstrapRunRecovery(){return restoreActiveRun()}
50
  async function restoreActiveRun(){try{const resumable=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(resumable?.run?.run_id){state.runId=resumable.run.run_id;if(resumable.bucket_source)state.bucketSource=resumable.bucket_source;setBuildModeInspecting(state.runId);if(resumable.view)renderRunViewModel(resumable.view,{progress:0,events:[]});setQuickLinks(resumable.view?.links||{});startPolling(state.runId);return true}}catch(e){console.warn('resumable lookup failed',e)}try{const raw=localStorage.getItem('asf.activeRun');if(!raw)return false;const saved=JSON.parse(raw);if(saved?.bucketName)state.bucketName=saved.bucketName;if(saved?.runId){state.runId=saved.runId;setText('detailRunId',saved.runId);setQuickLinks(saved.links||{});setBuildModeInspecting(saved.runId);startPolling(saved.runId);return true}}catch(_){}return false}
51
  async function loadAppInfo(){setOperation('Checking sign-in and run storage…','loading');try{const me=await apiGet('/api/me');state.user=me;setText('userChip','@'+me.username);const userChip=$('userChip');if(userChip)userChip.href=`https://huggingface.co/${me.username}`;const logout=$('logoutLink');if(logout){logout.hidden=false;logout.href=me.logout_url||'/logout'}state.bucketSource=me.username+'/'+state.bucketName;setText('bucketInlineLabel',state.bucketSource);renderAuthWarnings(me);await checkBucket(false);await loadRuns(true);clearSavedRun();if(!state.bucketReady)setOperation('Create your private run bucket before launching builds.','warning');else setOperation('')}catch(e){state.user=null;setText('userChip','Sign in');const userChip=$('userChip');if(userChip)userChip.href='/oauth/huggingface/login';const logout=$('logoutLink');if(logout)logout.hidden=true;renderSignedOut();setBucketReady(false,'Sign in with Hugging Face first.');showMessage('Please sign in with Hugging Face, then refresh this page.',true)}}
 
57
  function prepareValidationFromActiveRun(){const detail=state.selectedRunDetail;const summary=detail?.summary||{};const target=summary.target_space||$('detailSpace')?.textContent||'';if(target&&target!=='—')setInputValue('validateSpace',target);const expected=summary.expected_output_type||detail?.generation_smoke?.expected_output_type||formValue('buildExpectedOutput')||'image';setInputValue('expectedOutput',expected);setInputValue('validateApi','/generate');switchView('validate');showMessage(`Prepared validation form for ${target||'selected Space'}.`,'info')}
58
  async function launchBuild(){const formError=validateBuildForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching build Job…');setButtonBusy('launchBuild',true,'Launching…');try{setText('hardwarePlan',hardwarePlanLabel());const payload={bucket_name:state.bucketName,run_id:formValue('runId')||undefined,model_id:formValue('modelId'),target_space_name:formValue('targetSpace'),pi_model:formValue('piModel'),implementation_mode:formValue('implementationMode'),preferred_space_hardware:effectivePreferredHardware(),fallback_space_hardware:formValue('fallbackHardware'),allow_fixed_gpu_fallback:$('allowFallback')?.checked??true,expected_output_type:formValue('buildExpectedOutput')};const result=await apiPost('/api/build',payload);setActiveRun(result);showMessage(`Build launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchBuild',false)}}
59
  async function launchValidate(){const formError=validateValidationForm();if(formError){showMessage(formError,true);return}setBusy(true,'Launching validation Job…');setButtonBusy('launchValidate',true,'Launching…');try{const payload={bucket_name:state.bucketName,target_space_id:formValue('validateSpace'),api_name:formValue('validateApi')||'/generate',expected_output_type:formValue('expectedOutput')||'image',test_args_json:formValue('testArgs')||'[]',test_kwargs_json:formValue('testKwargs')||'{}',live_timeout_seconds:Number(formValue('validateTimeout')||1800)};const result=await apiPost('/api/validate',payload);setActiveRun(result);showMessage(`Validation launched: ${result.run_id}`,'success');loadRuns(true)}catch(e){showMessage(e.message,true)}finally{setBusy(false);setButtonBusy('launchValidate',false)}}
60
+ async function refreshProgress(){if(!state.runId||state.progressPollInFlight)return;state.progressPollInFlight=true;try{const now=Date.now();const includeLogs=!state.lastJobLogPoll||now-state.lastJobLogPoll>8000;if(includeLogs)state.lastJobLogPoll=now;const p=await apiGet(`/api/runs/${encodeURIComponent(state.runId)}/progress?bucket_name=${encodeURIComponent(state.bucketName)}${includeLogs?'&include_job_logs=1':''}`);state.pollErrors=0;const warn=$('pollWarning');if(warn)warn.hidden=true;if(p.bucket_source)state.bucketSource=p.bucket_source;renderProgress(p);renderEvents(p.events||[]);renderMetrics(p);if(p.view){setText('detailRunId',p.view.run_id||p.run_id);setText('selectedModelId',p.view.header?.model||p.summary?.model_id||'—')}else{setText('detailRunId',p.run_id||state.runId)}if(p.state){setText('detailSpace',p.state.target_space||p.summary?.target_space||'—');setText('detailHardware',p.state.selected_hardware||p.summary?.selected_hardware||'—')}setText('detailJobStatus',p.view?.header?.raw_status||p.state?.status||p.status||'running');setText('detailGateStatus',p.inference_gate?.status||p.summary?.gate_status||'—');setText('detailTraces',Array.isArray(p.events)?String(p.events.length)+' events':'—');setText('homeRunDetailStatus',p.view?.header?.status_label||p.state?.status||p.status||'running');setQuickLinks({...p.links,...p.view?.links,bucket_source:p.bucket_source,run_id:p.run_id});saveActiveRun();const status=String(p.view?.header?.status||p.status||p.state?.status||'').toLowerCase();if(TERMINAL_STATUSES.has(status)&&state.poll){clearInterval(state.poll);state.poll=null;setOperation(`Run finished with status: ${badgeLabel(status)}`,status.includes('success')||status.includes('succeed')?'success':status.includes('failed')||status.includes('blocker')?'error':'warning')}if(now-(state.lastRunsRefreshFromPoll||0)>15000){state.lastRunsRefreshFromPoll=now;loadRuns(false)}}catch(e){state.pollErrors+=1;console.warn(e);const warn=$('pollWarning');if(warn){warn.textContent=`Progress polling failed ${state.pollErrors} time${state.pollErrors===1?'':'s'}. The Job may still be running; retrying automatically…`;warn.hidden=false}}finally{state.progressPollInFlight=false}} function renderMetrics(p){const root=$('metricsList');if(!root)return;const smoke=p.generation_smoke||{};const gate=p.inference_gate||{};const health=(gate.implementation_signals&&gate.implementation_signals.health_passed)||smoke.health_passed;const smokeOk=smoke.ok||smoke.status==='success';const latency=smoke.latency_seconds||p.summary?.latency_seconds;const duration=smoke.recommended_zerogpu_duration_seconds||smoke.recommended_duration_seconds;root.innerHTML=`<li><span>/health</span><strong class="${health?'metric-ok':''}">${health?'passed':'pending'}</strong></li><li><span>/generate smoke test</span><strong class="${smokeOk?'metric-ok':'metric-warn'}">${smokeOk?'passed':(smoke.status||'pending')}</strong></li><li><span>Latency</span><strong>${latency?fmtSeconds(Math.round(latency)):'—'}</strong></li><li><span>Recommended ZeroGPU duration</span><strong>${duration?fmtSeconds(Math.round(duration)):'—'}</strong></li>`}
61
  function renderEvents(events){const root=$('eventsPanel');if(!root)return;root.textContent=(events||[]).slice(-12).map(e=>`${e.ts||''} ${e.step||''} ${e.status||''} ${e.message||''}`).join('\n')||'No events yet. The run may still be scheduling.'}
62
  async function cancelActiveRun(){
63
  if(!state.runId){showMessage('Select a running run before cancelling.', 'warning');return}
 
75
  setButtonBusy('cancelRun',false);
76
  }
77
  }
78
+ async function resumeLatestRun(){setButtonBusy('resumeLatestRun',true,'Resuming…');try{await loadRuns(true);const data=await apiGet(`/api/runs/resumable?bucket_name=${encodeURIComponent(state.bucketName)}`);if(!data.run){showMessage('No latest run found in this bucket.','warning');return}state.runId=data.run.run_id;if(data.bucket_source)state.bucketSource=data.bucket_source;setText('detailRunId',state.runId);setBuildModeInspecting(state.runId);setQuickLinks(data.view?.links||{});if(data.view)renderRunViewModel(data.view,{run_id:state.runId,progress:data.view.header?.status==='succeeded'?100:0,events:data.view.activity||[]});startPolling(state.runId);showMessage(`Resumed ${state.runId}`,'success')}catch(e){showMessage(e.message,true)}finally{setButtonBusy('resumeLatestRun',false)}}
79
  function switchView(view){state.currentView=view;document.querySelectorAll('.nav-item').forEach(b=>b.classList.toggle('active',b.dataset.view===view));document.querySelectorAll('[data-section]').forEach(s=>s.hidden=s.dataset.section!==view);setOperation('');const warn=$('pollWarning');if(warn)warn.hidden=true;if(view==='progress')loadRuns(false);const targetMap={build:'newBuildCard',progress:'runsExplorerPanel',validate:'spaceTestSection'};const target=$(targetMap[view]||'');if(target)target.scrollIntoView({behavior:'smooth',block:'start'})}
80
  function bindNavigation(){document.querySelectorAll('.nav-item').forEach(btn=>btn.addEventListener('click',()=>switchView(btn.dataset.view||'build')));document.querySelectorAll('[data-view-target]').forEach(btn=>btn.addEventListener('click',()=>switchView(btn.dataset.viewTarget||'build')))}
81
  function bindRunFilters(){document.querySelectorAll('.filter[data-status]').forEach(btn=>btn.addEventListener('click',()=>{state.runStatusFilter=btn.dataset.status||'all';state.runPage=0;renderRunsFromCache()}));for(const id of ['runSearch','runSearchFull'])$(id)?.addEventListener('input',e=>{state.runSearch=e.target.value||'';state.runPage=0;for(const other of ['runSearch','runSearchFull'])if($(other)&&$(other)!==e.target)$(other).value=state.runSearch;clearTimeout(state.runSearchTimer);state.runSearchTimer=setTimeout(renderRunsFromCache,80)})}
82
+ function bindActions(){$('launchBuild')?.setAttribute('data-requires-bucket','true');$('launchBuild')?.setAttribute('data-action-button','true');$('launchValidate')?.setAttribute('data-action-button','true');$('launchBuild')?.addEventListener('click',launchBuild);$('launchValidate')?.addEventListener('click',launchValidate);$('prepareNewBuild')?.addEventListener('click',prepareNewBuild);$('manualGoValidate')?.addEventListener('click',prepareValidationFromActiveRun);$('homeValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('selectedValidateBtn')?.addEventListener('click',prepareValidationFromActiveRun);$('checkBucket')?.addEventListener('click',()=>checkBucket(true));$('bucketStatusPill')?.addEventListener('click',()=>checkBucket(true));$('createBucket')?.addEventListener('click',createBucket);$('refreshRuns')?.addEventListener('click',()=>loadRuns(true));$('refreshRunsHome')?.addEventListener('click',()=>loadRuns(true));$('resumeLatestRun')?.addEventListener('click',resumeLatestRun);$('cancelRun')?.addEventListener('click',cancelActiveRun);$('resumeLatestRunFull')?.addEventListener('click',resumeLatestRun);['tryZeroGpu','preferredHardware','fallbackHardware','allowFallback'].forEach(id=>$(id)?.addEventListener('change',()=>setText('hardwarePlan',hardwarePlanLabel())))}
83
  bindNavigation();bindActions();bindRunFilters();setText('hardwarePlan',hardwarePlanLabel());setBuildModeNew();renderTimeline(DEFAULT_TIMELINE);loadAppInfo();
84
 
85
  // Pass 47 intentionally removed automatic switchView('progress') after launch and run selection.
web/static/progress.js CHANGED
@@ -1,5 +1,5 @@
1
  const timelineRenderState={};
2
- const activityFeedState={showAll:false,lastItems:[],runId:null};
3
  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','health_only','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']);
4
  const progressMotionState={lastProgress:null,lastStatus:null,lastStep:null};
5
  const progressStability={runId:null,progress:0,timeline:null,furthestIndex:-1,status:'',currentStep:null};
@@ -48,9 +48,7 @@ function mergeStableEvents(runId,items){
48
  if(activityFeedState.runId!==runId){
49
  activityFeedState.runId=runId;
50
  activityFeedState.lastItems=[];
51
- activityFeedState.showAll=false;
52
- const btn=$('viewAllEvents');
53
- if(btn)btn.textContent='View all events';
54
  }
55
  const byKey=new Map();
56
  [...activityFeedState.lastItems,...incoming].forEach(event=>{
@@ -74,17 +72,25 @@ function activeTimelineItem(items=[]){
74
  function eventMessage(event){
75
  return event?.message||event?.step||'Waiting for worker events.';
76
  }
77
- function centerTimelineOnStep(stepKey){
78
  const root=$('timeline');
79
  if(!root||!stepKey)return;
80
- if(root.dataset.centeredStep===stepKey)return;
81
  const safeStep=(typeof CSS!=='undefined'&&CSS.escape)?CSS.escape(stepKey):stepKey.replace(/"/g,'\\"');
82
  const target=root.querySelector(`[data-step="${safeStep}"]`);
83
  if(!target)return;
84
- root.dataset.centeredStep=stepKey;
85
- const left=target.offsetLeft-(root.clientWidth-target.clientWidth)/2;
86
- try{root.scrollTo({left:Math.max(0,left),behavior:'smooth'});}
87
- catch(_){root.scrollLeft=Math.max(0,left);}
 
 
 
 
 
 
 
 
 
88
  }
89
  function renderTimeline(items,overallStatus=''){
90
  const root=$('timeline');
@@ -92,7 +98,7 @@ function renderTimeline(items,overallStatus=''){
92
  const list=items||[];
93
  const currentKey=activeTimelineStepKey(list);
94
  const signature=list.map((x,i)=>`${timelineStepKey(x,i)}:${timelineStatus(x.status,overallStatus)}:${x.label||''}:${overallStatus||''}`).join('|');
95
- if(root.dataset.signature===signature){centerTimelineOnStep(currentKey);return;}
96
  root.dataset.signature=signature;
97
  const nextState={};
98
  root.innerHTML=list.map((x,i)=>{
@@ -112,30 +118,25 @@ function renderTimeline(items,overallStatus=''){
112
  const dot=step.querySelector('.step-dot');
113
  animateClass(dot,'dot-pop',700);
114
  });
115
- centerTimelineOnStep(currentKey);
116
  }
117
- function renderActivityFeed(items,forceAll=null){
118
  const root=$('activityFeed');
119
  if(!root)return;
120
  const list=mergeStableEvents(String(state.runId||''),items||[]);
121
- if(forceAll!==null)activityFeedState.showAll=Boolean(forceAll);
122
- const visible=activityFeedState.showAll?list:list.slice(-3);
123
- const signature=`${activityFeedState.showAll?'all':'compact'}|`+visible.map(item=>`${item.ts||''}:${item.step||''}:${item.status||''}:${item.message||''}`).join('|');
124
  if(root.dataset.signature===signature)return;
125
  root.dataset.signature=signature;
 
126
  if(!list.length){
127
  root.innerHTML='<div class="empty">No activity yet. Waiting for the first worker event.</div>';
128
  return;
129
  }
130
- root.classList.toggle('show-all',activityFeedState.showAll);
131
- root.innerHTML=visible.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('');
132
- }
133
- function toggleAllEvents(){
134
- activityFeedState.showAll=!activityFeedState.showAll;
135
- const btn=$('viewAllEvents');
136
- if(btn)btn.textContent=activityFeedState.showAll?'Show latest 3':'View all events';
137
- renderActivityFeed(activityFeedState.lastItems,activityFeedState.showAll);
138
  }
 
139
  function renderDiagnostics(d={},header={}){
140
  setText('diagBuildStatus',badgeLabel(d.build_status||'pending'));
141
  setText('diagBuildStep',header.current_phase_label||'Waiting for run data');
@@ -235,7 +236,7 @@ function resetProgressStability(runId=null){
235
  progressMotionState.lastStatus=null;
236
  progressMotionState.lastStep=null;
237
  const timeline=$('timeline');
238
- if(timeline){timeline.dataset.signature='';timeline.dataset.centeredStep='';}
239
  }
240
  function stabilizeProgressPayload(p={},view=null){
241
  const runId=String(p.run_id||view?.run_id||state.runId||'');
 
1
  const timelineRenderState={};
2
+ const activityFeedState={lastItems:[],runId:null};
3
  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','health_only','repair_success','repair_failed','succeeded','waiting_manual_action','blocked','stale','cancelled','canceled']);
4
  const progressMotionState={lastProgress:null,lastStatus:null,lastStep:null};
5
  const progressStability={runId:null,progress:0,timeline:null,furthestIndex:-1,status:'',currentStep:null};
 
48
  if(activityFeedState.runId!==runId){
49
  activityFeedState.runId=runId;
50
  activityFeedState.lastItems=[];
51
+
 
 
52
  }
53
  const byKey=new Map();
54
  [...activityFeedState.lastItems,...incoming].forEach(event=>{
 
72
  function eventMessage(event){
73
  return event?.message||event?.step||'Waiting for worker events.';
74
  }
75
+ function ensureTimelineStepVisible(stepKey){
76
  const root=$('timeline');
77
  if(!root||!stepKey)return;
 
78
  const safeStep=(typeof CSS!=='undefined'&&CSS.escape)?CSS.escape(stepKey):stepKey.replace(/"/g,'\\"');
79
  const target=root.querySelector(`[data-step="${safeStep}"]`);
80
  if(!target)return;
81
+ const margin=18;
82
+ const visibleLeft=root.scrollLeft;
83
+ const visibleRight=visibleLeft+root.clientWidth;
84
+ const targetLeft=target.offsetLeft;
85
+ const targetRight=targetLeft+target.offsetWidth;
86
+ let nextLeft=null;
87
+ if(targetLeft<visibleLeft+margin)nextLeft=Math.max(0,targetLeft-margin);
88
+ else if(targetRight>visibleRight-margin)nextLeft=Math.max(0,targetRight-root.clientWidth+margin);
89
+ if(nextLeft!==null){
90
+ try{root.scrollTo({left:nextLeft,behavior:'smooth'});}
91
+ catch(_){root.scrollLeft=nextLeft;}
92
+ }
93
+ root.dataset.visibleStep=stepKey;
94
  }
95
  function renderTimeline(items,overallStatus=''){
96
  const root=$('timeline');
 
98
  const list=items||[];
99
  const currentKey=activeTimelineStepKey(list);
100
  const signature=list.map((x,i)=>`${timelineStepKey(x,i)}:${timelineStatus(x.status,overallStatus)}:${x.label||''}:${overallStatus||''}`).join('|');
101
+ if(root.dataset.signature===signature){ensureTimelineStepVisible(currentKey);return;}
102
  root.dataset.signature=signature;
103
  const nextState={};
104
  root.innerHTML=list.map((x,i)=>{
 
118
  const dot=step.querySelector('.step-dot');
119
  animateClass(dot,'dot-pop',700);
120
  });
121
+ ensureTimelineStepVisible(currentKey);
122
  }
123
+ function renderActivityFeed(items){
124
  const root=$('activityFeed');
125
  if(!root)return;
126
  const list=mergeStableEvents(String(state.runId||''),items||[]);
127
+ const visible=[...list].reverse();
128
+ const signature=visible.map(item=>`${item.ts||''}:${item.step||''}:${item.status||''}:${item.message||''}`).join('|');
 
129
  if(root.dataset.signature===signature)return;
130
  root.dataset.signature=signature;
131
+ root.classList.add('show-all');
132
  if(!list.length){
133
  root.innerHTML='<div class="empty">No activity yet. Waiting for the first worker event.</div>';
134
  return;
135
  }
136
+ root.innerHTML=visible.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('');
137
+ root.scrollTop=0;
 
 
 
 
 
 
138
  }
139
+
140
  function renderDiagnostics(d={},header={}){
141
  setText('diagBuildStatus',badgeLabel(d.build_status||'pending'));
142
  setText('diagBuildStep',header.current_phase_label||'Waiting for run data');
 
236
  progressMotionState.lastStatus=null;
237
  progressMotionState.lastStep=null;
238
  const timeline=$('timeline');
239
+ if(timeline){timeline.dataset.signature='';timeline.dataset.visibleStep='';}
240
  }
241
  function stabilizeProgressPayload(p={},view=null){
242
  const runId=String(p.run_id||view?.run_id||state.runId||'');